triage-mcp
Provides tools for triaging GitHub issues: searching issues, retrieving issue details, listing labels, and, when writes are enabled, adding labels and posting comments. It can operate against the GitHub REST API or a point-in-time snapshot of a repository's issues.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
triage-agent
An LLM agent that triages new GitHub issues by picking a type label and area labels and finding duplicates. It works through its own MCP server and is benchmarked against what the maintainers of a real project actually did.
The benchmark replays 120 real issues from astral-sh/uv, 40 of which the maintainers closed as duplicates. Each one is triaged against a 10k-issue snapshot of the tracker as it was on the day the issue was filed. All runs are local models on a laptop RTX 4050 (6 GB). Full table: eval/results/results.md.
Results
Model | Strategy | Type accuracy | Duplicate recall | Duplicate precision | False-duplicate rate | Tool calls | p50 latency | Tokens in |
llama3.2:3b | zero-shot | 73.9% | 0% | n/a | 0% | 0 | 1.4 s | 1.1k |
llama3.2:3b | rag | 75.7% | 2.5% | 50.0% | 1.2% | 0 | 1.7 s | 1.4k |
llama3.2:3b | agent | 51.3% | 0% | 0% | 18.8% | 2.7 | 10.2 s | 10.7k |
qwen3:4b-instruct | zero-shot | 76.5% | 0% | n/a | 0% | 0 | 3.3 s | 1.1k |
qwen3:4b-instruct | rag | 76.5% | 15.0% | 37.5% | 8.8% | 0 | 3.7 s | 1.4k |
qwen3:4b-instruct | agent | 72.2% | 22.5% | 22.5% | 27.5% | 3.4 | 9.1 s | 10.0k |
Type accuracy is over the 115 issues with exactly one type label. Duplicate recall is over the 40 duplicates. The false-duplicate rate is over the 80 issues that weren't duplicates.
What the numbers say:
Only an agent with a capable model finds duplicates beyond what one search can surface. A single title search puts the true duplicate in the top 5 for just 13 of 40 duplicates (32%), so that is the ceiling for
rag. qwen's agent ran several searches of its own choosing and found 9 of 40 (22.5%), the best recall in the table.That recall costs precision. Having seen similar issues, the 4B agent calls "duplicate" too readily: it flagged 22 of the 80 non-duplicates (27.5%).
raghas the best precision at 37.5%. As shipped, a human approves every comment, and a duplicate is only suggested, never closed. That is the right default at this precision.The extra context costs some type accuracy. On both models the agent's type accuracy is below the single-turn strategies, with about 7× the input tokens and 3–6× the latency.
A 3B model can't run the loop. llama3.2:3b had to be forced to submit on 67% of issues, ticked so many area labels that 46% of its area sets were rejected, and dropped to 51% type accuracy. For a model this size, plain
ragis the right choice. The harness guardrails (forced structured submit, label validation, area cap) are what kept its output well-formed at all.Area labels are weak ground truth. uv maintainers apply
area:labels to a small minority of issues, so area precision is low for every configuration. Some of those "false positives" are probably reasonable labels nobody applied. Area metrics are in the full table but aren't used to compare strategies.
What changed after the first run
The first agent run (kept in eval/results-v1) exposed problems that only show up with real model output:
Lists sent as strings. llama3.2 sometimes passed
areasas the string"['area:cli']". The validator iterated over it character by character and logged hundreds of one-letter "invalid labels". The validator now parses JSON-ish strings and comma lists (_as_list).Searching only open issues. The model set
state: "open"on nearly every search, and duplicates usually point at an issue that is already closed. The tool description now says to leavestateasall, andlimitis clamped to at least 3 because the model kept asking for a single hit.Submitting without looking. In v1, 82 of 120 agent runs submitted without a single tool call. The agent now gets one rejection ("search existing issues at least once") if it tries to submit before searching.
Runaway generations. One forced submit generated for minutes. Ollama calls now cap
num_predict.Every area at once. Under constrained decoding the 3B model sometimes listed all 34 area labels. More than 3 areas is now treated as noise and dropped (
areas_rejected).
After these fixes, llama's agent searched on 39 of the 40 duplicates, up from 4 of 40 in v1. It still can't make good use of what it finds, and the table above says so rather than hiding it. qwen was only benchmarked after the fixes.
Related MCP server: github-mcp-server
How it works
flowchart LR
I[New issue] --> A
subgraph Agent loop
A[LLM<br/>Ollama or Claude] -- search_issues / get_issue --> M
M -- compact text results --> A
end
M[MCP server<br/>triage-mcp] --> B{Backend}
B --> G[GitHub REST API]
B --> S[Snapshot<br/>scoped to a point in time]
A -- submit_triage --> V[Validate<br/>labels exist, duplicate exists]
V --> P[Planned actions<br/>add_labels, post_comment]
P -- human approves each --> MThe MCP server (triage-mcp) exposes search_issues, get_issue and list_labels, plus add_labels and post_comment when writes are enabled. It is a normal stdio MCP server, so Claude Desktop, Claude Code or any other MCP client can use it, not just this agent. Writes have three modes: off (the tools aren't registered at all), dry-run (they say what they would do) and live.
The agent never writes anything itself. It investigates with read tools and then calls submit_triage, a structured decision with a type, areas, duplicate_of and a summary. The harness then:
Validates the decision. Labels that don't exist are dropped and counted. A
duplicate_ofis checked against the tracker through the MCP server, so a made-up issue number never reaches a comment.Plans the writes the decision implies: add labels, and post a comment naming the possible duplicate.
Asks a human to approve each write before it goes through the MCP server. Closing an issue is deliberately not a tool.
Reliability with small models. A 3B local model doesn't always call tools when it should. The loop handles this:
If the model answers in prose instead of calling a tool, it gets a nudge.
If it runs out of tool budget, it is forced to submit. Claude does this through
tool_choice. Ollama does it through a JSON-schema constrained response, so the forced decision is always well-formed, with labels restricted to the real label set by anenum.
Three strategies, one code path
Strategy | What the model gets | Tool calls |
| The issue and the label set | none |
| The issue, the label set, and the top 5 title-search hits, retrieved by the harness | 0 by the model |
| The issue, the label set, and | up to 4, chosen by the model |
They share the prompt, the validation and the scoring. The only difference is who decides what to look at.
How the benchmark works
Ground truth is what the maintainers did.
triage fetchsnapshots every issue in a repository through the GitHub GraphQL API. The benchmark samples issues the maintainers already triaged: the type label they applied, anyarea:labels, and, for issues they closed as duplicates, which issue they pointed to (GitHub's own duplicate link, else a "Duplicate of #N" comment).No peeking at the future. Each issue is triaged against the tracker as it was when that issue was filed. Later issues don't exist, issues closed later show as open, and the issue itself is hidden. Without this, the agent could search its way to the answer.
No comments. The snapshot leaves comments out entirely, because on a duplicate the comments usually contain the answer.
Mechanical metrics only. Type accuracy, duplicate recall and precision, false-duplicate rate on issues that weren't duplicates, area precision and recall, invalid labels, forced submits, tool calls, latency and tokens. No LLM judge is involved.
Quickstart
pip install -e ".[dev]" # add ,anthropic for Claude
ollama pull llama3.2:3b
# Triage a live issue (dry run: nothing is written)
triage run astral-sh/uv 12345
# Write for real, approving each action
triage run your-org/your-repo 42 --applyUse the MCP server from Claude Desktop or Claude Code
{
"mcpServers": {
"issue-triage": {
"command": "triage-mcp",
"args": ["--repo", "astral-sh/uv", "--writes", "dry-run"]
}
}
}Reproduce the benchmark
triage fetch astral-sh/uv # ~10k issues, about 6 minutes
triage build-eval --snapshot data/astral-sh__uv --out eval/datasets/uv.jsonl
triage eval --snapshot data/astral-sh__uv --dataset eval/datasets/uv.jsonl --provider ollama --model llama3.2:3b
triage report --snapshot data/astral-sh__uv --dataset eval/datasets/uv.jsonltriage eval appends to one JSONL per model and strategy and resumes where it stopped. Each record holds the full decision, every tool call with its arguments and latency, token counts, and the gold labels.
Project layout
src/triage/
mcp_server.py MCP server: tools, write modes, stdio entry point
backends.py GitHub REST backend and the read-only snapshot backend
store.py Issue snapshot with BM25 search, scoped to a point in time
agent.py Triage loop, strategies, decision validation, action planning
llm.py Tool-calling adapters for Ollama and Claude
eval.py Eval-set sampling, the benchmark runner, metrics and report
fetch.py GraphQL snapshotter (issues, labels, duplicate links)
cli.py `triage` command
tests/ Store scoping, MCP server, agent loop with a scripted model, metrics
eval/ Dataset (issue numbers + gold labels) and resultsLimitations and next steps
One repository, 120 issues, small local models. The confidence intervals are wide (±8 points on type accuracy). The next step is a frontier model (
--provider anthropic) and a second repository with a different labeling culture.BM25 only. Duplicates are often worded differently from the issue they duplicate. Adding the dense and hybrid retrieval measured in AskDocs to
search_issuesshould raise the 32% single-search ceiling.Duplicate precision. A verification step (re-read the candidate and answer "same root cause?" as a separate yes/no call) is the obvious fix for the agent's false duplicates.
Snapshot labels are current, not historical. Issues that existed when the target was filed show today's labels, which can include labels added later. State is rolled back (closed later → open); labels aren't.
License
MIT
This server cannot be deployed
Maintenance
Related MCP Connectors
GitHub MCP — wraps the GitHub public REST API (no auth required for public endpoints)
Manage repositories, users, releases, and automate GitHub workflows
Create, deploy, and operate MCP servers directly from your GitHub repositories.
Access the GitHub API, enabling file operations, repository management, search functionality, and…
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables GitHub API integration via MCP, allowing search, repository details, file contents, and issue management.811 npmISC
- AlicenseNot gradedqualityDmaintenanceEnables management of GitHub issues, milestones, projects, and labels for a single repository via MCP tools.811 npm1ISC
- FlicenseNot gradedqualityDmaintenanceEnables natural language interaction with GitHub through an MCP server. Supports reading repos, issues, and PRs, as well as creating and editing issues with user confirmation.-
- AlicenseNot gradedqualityDmaintenanceProvides read-only access to GitHub repositories and issues through standardized MCP tools, with caching to handle API rate limits.1MIT