- name
- issue-triage
- description
- Triage this repo's inflow — sweep unboarded issues onto the Servers V2 board (#43) as Incoming, score Priority with the rubric and post the score, classify outside PRs and spam (server listings, new servers, duplicates, no-ops) and close them with their canned comments, and run the board audit. Use for "triage new issues" or "triage the PRs"; when scoring an issue's Priority; when answering or closing an outside PR; or when auditing the board for drift.
- disable-model-invocation
- false
# Triaging issues and outside PRs
This repository receives far more outside issues and pull requests than it
opens itself, much of it misdirected (server listings, new servers, questions
for the SDK) and much of it racing (several PRs for the same bug). This skill is
the procedure for all of it: classify first, answer the classes that do not
belong here with a canned response, sweep the rest onto the board unapproved,
and check the board afterwards.
The **rules** it carries out are in [`AGENTS.md`](../../../AGENTS.md), under
**Contributing** and **Issue-driven work style**. The board's IDs, and the
recipes that add, move and delete cards, are in `board-ops`, and only there:
**load `board-ops` before the first board write.** Labels for an issue you file
come from `issue-create`. The plan for the outside-PR backlog is
[`docs/contribution-model.md`](../../../docs/contribution-model.md), "The plan";
[Outside PRs](#outside-prs) below is how it is carried out.
**"Triage new issues" means:** the [class check](#step-0--classify-before-boarding)
and [pass 1](#pass-1--sweep-onto-the-board-no-approval-implied) over every
unboarded open issue, then the [board audit](#the-board-audit). **"Triage the
PRs" means:** snapshot and classify the open outside PRs into a manifest and
stop for a maintainer's review ([Outside PRs](#outside-prs)). Pass 2 and every
close of an outside PR need a human, and neither is done unprompted.
The one-time sweep of the existing backlog is tracked in #4875 (#4876 for
issues, #4877 for PRs). It uses this skill's classes and responses, plus the
conventions that tracker sets for the sweep, such as a close label and a
reference to #4875 in each close comment.
## Outward-facing actions
Every label, comment and close lands on someone else's issue or PR, and a
reporter is notified of each one. So:
- **A batch of closes happens only after a maintainer has reviewed the
manifest** that lists each item, its class, and the issue it will point at.
A wrong class is cheap to fix in a file and expensive to fix after a
notification went out. Closing a single item the maintainer named is fine.
- **Pace every write loop**: a few seconds between calls, and **stop on the
first failed call** rather than carrying on blind. GitHub's secondary rate
limit on content creation trips well before its primary one, and a loop that
keeps going after a 403 leaves a half-applied batch nobody can see.
- **Never restate a vulnerability in public**, not even to summarize a report
that was already filed publicly. See [Security reports](#security-reports).
- **Keep dumps outside the repository.** Every snapshot and manifest below goes
in a `mktemp -d` directory. The board is private, and a dump left in the
working tree is one `git add -A` away from being published.
## Step 0 — classify before boarding
Read each unboarded issue before sweeping it in. Most are real reports, but some
belong elsewhere, and boarding one of those would park it in a review queue for
work this repository does not do. A class whose Action is **Close** is closed
with its [canned response](#canned-responses), gets **no card** (`Done` means
shipped, and closed-not-planned work never goes on the board), and needs no
labels. A security report is never closed by triage.
| Class | Recognized by | Action | Response |
| --- | --- | --- | --- |
| **Server listing or submission** | Asks for a server to be listed here, in the README or `ADDITIONAL.md` | Close, `not planned` | [Registry, issue](#registry-issue) |
| **New server request** | Asks this repository to build or host a new server | Close, `not planned` | [Registry, issue](#registry-issue) |
| **Archived server** | About a server in [`servers-archived`](https://github.com/modelcontextprotocol/servers-archived) (GitHub, Slack, Postgres, Puppeteer, …) | Close, `not planned` | [Archived](#archived) |
| **SDK or specification** | Reproduces against the SDK directly, or asks for a protocol change | Close, `not planned` | [Elsewhere](#elsewhere) |
| **Duplicate** | An open or closed issue already reports it (search all states, as in `issue-create` step 0) | Close as `duplicate` (the `board-ops` API call); delete its card if it has one | [Duplicate](#duplicate) |
| **Spam or empty** | No content, unrelated or generated content | Close, `not planned` | None |
| **Security report** | Describes a vulnerability in one of the seven servers | **Stop. A maintainer decides** | [Security reports](#security-reports) |
| **Everything else** | A bug, a usability problem, an enhancement, a question about these servers | [Pass 1](#pass-1--sweep-onto-the-board-no-approval-implied) | None; the score comment |
When a class is a judgment call (a feature a maintainer might still take, a
report that could be the SDK or the server), sweep it in rather than closing
it. Pass 1 approves nothing, and a maintainer reading `Incoming` can still close
it; a wrong close cannot be quietly undone. For the same reason, closes follow
[Outward-facing actions](#outward-facing-actions): when a run finds more than
one, list them with their classes for a maintainer to confirm, and go on with
pass 1 for the rest meanwhile.
```sh
# The run's temp dir; create it once, before the first close, and reuse it.
D=${D:-$(mktemp -d)}
# Copy the class's response from Canned responses into "$D/response.md" (the
# text inside the quote, with #ORIGINAL or the SDK link filled in) first.
# Close as not planned, with that response (body in a file, so quoting is
# safe). NOT for a duplicate: post the Duplicate response with `gh issue
# comment`, close it with the `board-ops` state_reason=duplicate call, and
# delete its card if it has one (`board-ops`, Delete a card). An empty or missing response stops here instead of closing silently.
BODY=$(cat "$D/response.md") && [ -n "$BODY" ] \
&& gh issue close <N> --repo modelcontextprotocol/servers --reason "not planned" --comment "$BODY" \
|| { echo "no response or close failed for #<N> — stop and check it" >&2; false; }
# Spam or empty, the one class with no response:
gh issue close <N> --repo modelcontextprotocol/servers --reason "not planned"
```
## Pass 1 — sweep onto the board (no approval implied)
**An issue needs triage when it has no card on #43, whoever filed it.** The test
is about state, not authorship. An outside reporter cannot board anything, but a
maintainer who opens an issue in the web UI instead of through `issue-create`
leaves it unboarded too, and write access makes the board reachable, not
automatic. The milestone is not part of the test; it decides where the card
lands.
For each unboarded open issue left after step 0:
1. **Labels.** `v2`, **exactly one** type label (`bug` / `enhancement` /
`documentation` / `chore` / `question`; the table is in `issue-create`), and
the server's **scope label** (`server-<name>`) when it concerns one server.
The issue forms apply `v2` and `bug` or `enhancement` on their own, but not
the scope label, which triage reads off the form's server dropdown. Check
the preset type too: a "bug report" that is a usage question is `question`.
2. **Board it** with the `board-ops` add-card recipe, Status **`Incoming`**.
3. **Priority** from [the rubric](#the-priority-rubric), and **post the score as
a comment** ([Recording the score](#recording-the-score)). That is an
assessment for ordering the queue, not an approval.
4. **Leave the milestone unset.** An empty milestone is what marks it as
awaiting review.
**The one exception is an unboarded issue that already has a milestone.**
Someone recorded the approval and only the card is missing, so it goes straight
into **`Todo`**, keeping the milestone. Otherwise triage would silently
un-approve work a maintainer had already scheduled.
### Finding the unboarded ones
```sh
D=$(mktemp -d); R=modelcontextprotocol/servers
# --limit has headroom over the repo's open issues, and a listing that fills it
# is treated as truncated: gh does not say when it stops early.
gh issue list --repo $R --state open --limit 5000 \
--json number,title,milestone,labels > "$D/open.json"
jq -e 'length < 5000' "$D/open.json" >/dev/null \
|| { echo "issue listing truncated or failed — raise --limit" >&2; rm -f "$D/open.json"; }
# item-list truncates SILENTLY past --limit, and a card missing from the dump
# reads as an unboarded issue that then gets a second card.
gh project item-list 43 --owner modelcontextprotocol --format json --limit 2000 > "$D/b43.json"
jq -e '(.items | length) == .totalCount' "$D/b43.json" >/dev/null \
|| { echo "board #43 listing INCOMPLETE — raise --limit and re-run" >&2; rm -f "$D/b43.json"; }
# An org board can hold other repositories' issues; keep this repo's only.
jq -r --slurpfile b "$D/b43.json" --arg R "$R" '
[$b[0].items[] | select(.content.type=="Issue" and .content.repository==$R)
| .content.number] as $boarded
| .[] | select(.number as $n | $boarded | index($n) | not)
| "#\(.number)\t→ \(if .milestone then "Todo (has milestone \(.milestone.title))" else "Incoming" end)\t\(.title[0:70])"' \
"$D/open.json"
```
A failed or truncated listing deletes its file, so the last step fails on the
missing file instead of reporting every issue as unboarded.
The finder is only half the resume point. An issue whose card was added but
whose Priority or score comment then failed has left the finder's list, and the
audit catches a missing Priority but not a missing comment. So after an
interrupted batch, **finish the last issue you were working on first** (read its
card back with `board-ops` and check its comments), then resume from the
finder's output, never from memory.
## Pass 2 — approve what should ship
A maintainer reads the `Incoming` column and, for each issue worth doing,
**assigns a milestone** and moves the card to **`Todo`** (or **`In Progress`**
when picking it up now). That is the whole approval gesture.
**Pass 2 is reserved for a human.** Deciding what ships in which release is not
something to infer from a rubric score, so never promote a card out of
`Incoming` as part of a routine sweep. An issue that should not ship stays in
`Incoming` or is closed, and a closed one has its card deleted.
## The priority rubric
Score it rather than assert it: rate two axes 1–5, add the signal bonuses, and
read the band off the table. Two people triaging the same issue should land in
the same place, and the reasoning should survive in a form someone can argue
with later.
**Axis 1: severity and impact (1–5).** How bad is it when it happens?
| Score | Means |
| --- | --- |
| 1 | Cosmetic: a typo in a README or a tool description, a wording nit. |
| 2 | Minor friction with an easy workaround. |
| 3 | A tool, resource or prompt is broken or missing, and the workaround is annoying or partial. |
| 4 | A server's core function is unusable, or a server **reports something false about the protocol** (a result or schema a conforming client rejects or misreads). |
| 5 | Data loss; a security vulnerability, such as an operation that **escapes an allowed root**, SSRF, or command or argument injection; or a release broken on arrival for everyone. |
**Axis 2: urgency and staleness (1–5).** How time-sensitive or neglected is it?
| Score | Means |
| --- | --- |
| 1 | No time pressure; nothing waits on it. |
| 2 | Wanted eventually. |
| 3 | Wanted this milestone, or has sat more than 90 days with no activity. |
| 4 | Blocking other work, or tied to a dated external dependency (an SDK release, a spec revision's date). |
| 5 | Blocking a release, or hurting users of a published version right now. |
**Signal bonuses (+1 each, not an axis of their own).** Corroborating evidence
the two axes may have undercounted:
- Carries the `bug` label, or concerns security.
- Has a milestone, meaning it is **already approved**. This is the
_re-scoring_ case: an issue scored in pass 1 has no milestone by rule, so it
never earns this bonus. If every issue in a batch is earning it, the milestone
is being treated as a formality.
- High engagement (many comments, reactions, or linked outside PRs racing to
fix it).
- Assigned to someone.
- A sub-issue of a larger tracker.
- The reporter set **Fields → Priority** to `Urgent` or `High`: **+1, flat**,
whichever of the two.
**Bands.** The axes give 2–10 and there are six bonuses, so totals run 2–16.
| Total | Priority | Meaning |
| --- | --- | --- |
| 12+ | **Urgent** | Drop what you're doing. |
| 9–11 | **High** | Next up after current work. |
| 6–8 | **Medium** | Scheduled normally. |
| ≤5 | **Low** | Nice to have; may sit. |
Severity alone does not reach Urgent: a 5/5 with no corroborating signal totals
10 and lands **High**. Urgent is for a severe problem that something _else_ also
says is burning. Override the band when it is plainly wrong, and say why in the
score comment; a rubric nobody may overrule is a rubric people route around.
### Trust boundary: who can set what
**Board #43 is private.** Its Status and Priority are visible only to people
with project access. It is the maintainers' working queue, not a published
commitment. The **score comment** below is public, though: it is how the
reasoning survives, so write it for the reporter to read too.
**Fields → Priority** on the issue page is the opposite: an org-level issue
field, public, and settable by people outside maintainer triage (the two fields
are compared in `board-ops`). A value there is a preference, not an assessment,
so it earns the flat +1 and nothing more. It can lift an issue at most one band,
and nothing a reporter sets reaches Urgent by itself, because Urgent needs 12
and the issue would already need 11 from the maintainer-assessed axes (at most
10) plus at least one other bonus. **Never copy
the value across:** a reporter choosing `Urgent` does not make the card Urgent,
or the queue would sort by assertiveness instead of impact.
### Recording the score
The board stores only the result, and the board is private. So **post the
arithmetic as a comment on the issue**; without it, a later re-scoring cannot
tell a considered judgment from a guess.
```sh
gh issue comment <N> --repo modelcontextprotocol/servers --body \
'**Triage:** Priority **Medium** (total 7)
- Severity 3: `directory_tree` fails on a symlinked root; workaround is listing by hand
- Urgency 2: wanted eventually, nothing blocked on it
- Bonuses: +1 `bug` label, +1 reporter set Fields → Priority to High
Board: Servers V2 (#43), Status `Incoming`, awaiting maintainer review (no milestone yet).'
```
Name each bonus you claimed rather than only summing them. The milestone bonus
in particular should be conspicuously absent from a pass-1 comment; a comment
that lists it shows the approval semantics were misapplied.
## Security reports
A vulnerability in one of the seven servers is reported privately, through the
[advisory form](https://github.com/modelcontextprotocol/servers/security/advisories/new),
and worked in the `security-advisory` flow (#4870). When one arrives as a public
issue or a public PR anyway:
- **Do not restate or discuss the details**, in a comment, in a harvest issue,
or in a triage score. The score comment for such an issue names only the
bands.
- **Check the private advisory backlog first** (a maintainer, or an agent with
access, reads it; the listing is private data and stays out of the repo). A
public report of something already under advisory must not be answered in a
way that confirms the advisory.
- **Hand it to a maintainer.** Whether to close it, move it into a private
advisory, or fix it in the open is theirs to decide. The response below is
the one they usually post.
<a id="security-response"></a>
> Thank you for reporting this. Please don't add further details here: this
> repository takes vulnerability reports privately, through
> [the advisory form](https://github.com/modelcontextprotocol/servers/security/advisories/new).
> A maintainer will follow up.
## Outside PRs
**An outside PR is closed, not reviewed or merged**, with a pointer to the issue
flow, after anything worth keeping in it is tracked by an issue (`AGENTS.md`,
Contributing). An outside PR is any open PR whose author is **not a repository
maintainer and not Dependabot**. A maintainer is an author whose role on this
repository is `admin` or `maintain`; write access alone is not enough, which is
the policy's point.
**A Dependabot PR should not exist.** Dependency updates are issue-driven
(`AGENTS.md`, Dependencies): `.github/dependabot.yml` is gone and security-update
PRs are switched off, and three scheduled sweeps file issues instead. One that
appears anyway means a config came back or the `automated-security-fixes`
setting was switched on again. Close it with a pointer to the sweep issue that
covers the same bump, and raise the cause with a maintainer. If no sweep issue
covers it yet, do **not** file one with `issue-create`: the sweeps trust only
issues the automation wrote, so the next run would file a duplicate beside it.
Dispatch the sweep instead (`gh workflow run dependabot-alerts.yml`, or
`dependency-refresh.yml` for a version bump) and point at the issue it files.
⚠️ **Until the milestone merge carries the sweeps to `main`**, none of this
applies yet: a workflow can only be dispatched or scheduled from the default
branch, and `main` still has `dependabot.yml`. Leave Dependabot PRs open until
then. The merge PR closes them, each with the pointer #5065 records.
Not touched by triage:
- **Maintainer-authored PRs.** List any that target `main` for their authors to
retarget or close; do nothing else with them.
- **An outside PR an open issue already names as the thing to decide** (for
example #3260 under #4860). That issue owns its fate; leave the PR open and
name it in the report.
### 1. Snapshot and pre-classify
```sh
D=$(mktemp -d); R=modelcontextprotocol/servers
Auf GitHub ansehen