triage-mcp

triage-mcp

Enables LLMs to triage GitHub issues by retrieving similar reported issues and classifying components using historical data.

Category
访问服务器

README

triage-mcp

Issue triage for a busy GitHub repository is repetitive human work: for each new issue, someone checks whether it duplicates an existing report and routes it to the right component. triage-mcp turns a repository's history into a local retrieval-and-classification service and exposes it to an LLM over the Model Context Protocol, so the model can answer "has this been reported before?" and "which component owns this?" from real evidence. The server itself makes no model calls — it returns retrieved issues, similarity scores and predictions, each carrying the issue numbers it was derived from, and the client's LLM does the reasoning.

Built as a one-day, local-first project: no Docker, no database server, no managed vector store. State is Parquet, a NumPy matrix and a pickled scikit-learn estimator on disk.

Every number in this README is produced by a single evaluation run and is reproducible with make eval (seed 20260720). The committed run lives in results/20260720T201357Z/.


Architecture

flowchart LR
    GH["GitHub REST API<br/>(issues + PRs)"]
    P[("data/issues.parquet<br/>47,804 issues")]
    E[("embeddings.npy<br/>47,804 x 384")]
    EV["evals.py<br/>time-split harness"]
    R[("results/ per run<br/>metrics.json<br/>classifier.joblib")]
    S["server.py<br/>FastMCP (stdio)"]
    C["LLM client<br/>Claude Desktop / Code"]
    U(["Grounded triage<br/>with cited issues"])

    GH -->|"ingest.py: drop PRs, paginate, checkpoint"| P
    P -->|"store.py: all-MiniLM-L6-v2"| E
    P --> EV
    E --> EV
    EV -->|writes| R
    P --> S
    E --> S
    R -->|"fitted classifier"| S
    S <-->|"MCP tools: grounded JSON + scores"| C
    C --> U

Four stages, each its own module:

Stage Module What it produces
Ingest ingest.py Harvests issues via the GitHub REST API, drops pull requests, paginates with per-page checkpointing, validates each record with Pydantic → data/issues.parquet
Embed store.py Encodes title + body with all-MiniLM-L6-v2, content-hash cached, row-aligned with the corpus → data/embeddings.npy
Evaluate evals.py Builds a time-split classification task, scores three methods and a retrieval proxy → results/<ts>/ + the fitted classifier
Serve server.py Exposes six grounded tools over MCP/stdio; loads corpus, vectors and classifier lazily

Quickstart

Requires Python 3.11+ and uv.

make setup                                    # uv sync (pinned deps)
cp .env.example .env                          # add a GITHUB_TOKEN (raises the REST rate limit)

make ingest REPO=microsoft/vscode MAX=3000    # -> data/issues.parquet
make embed                                    # -> data/embeddings.npy  (content-hashed; re-runs are free)
make eval                                     # -> results/<ts>/ + data/classifier.joblib
make serve                                    # run the MCP server on stdio

The GitHub list endpoint caps at 10,000 items (~2 months for a repo this busy). The committed corpus is a full year, harvested by walking creation-date windows through the Search API:

uv run python -m triage_mcp.ingest --via search --since 2025-07-20

Other targets: make stats (corpus summary), make smoke (spawn the server and exercise every tool), make test, make lint.


Results

From results/20260720T201357Z/metrics.json. Corpus: 47,804 microsoft/vscode issues, split by creation date into 38,243 train (2025-07-20 → 2026-04-14) and 9,561 holdout (2026-04-14 → 2026-07-20).

Component classification (holdout)

method accuracy macro-F1 weighted-F1 p50 latency p95 latency
majority baseline 0.9485 0.1217 0.9235
TF-IDF + logistic regression 0.7566 0.3070 0.8300 0.92 ms 2.01 ms
embedding kNN (k=10) 0.9407 0.2310 0.9274 4.97 ms 6.84 ms

macro-F1 by method

Read accuracy with suspicion here. The majority baseline scores 0.9485 accuracy by predicting other for everything — because other is 9,069 of the 9,561 holdout issues (94.8%). Its macro-F1 is 0.1217. Macro-F1 is the honest comparison metric, and by it, TF-IDF + logistic regression (0.3070) is the best method — more than double the majority baseline and ahead of embedding kNN (0.2310). Latency is measured per query, one at a time, as the model would be served; it is hardware-dependent and not seeded, unlike the quality metrics.

Per-class F1 for the winning method shows where the signal is (full table in summary.md): chat-billing 0.494 and accessibility 0.413 are learnable; chat 0.036 is not — vscode spreads chat work across many chat-* labels and applies the bare chat label inconsistently, so it is a property of the label taxonomy, not the model.

confusion matrix

Retrieval proxy — label-match precision@5

This is a proxy, not a duplicate-detection rate. It measures how often a retrieved neighbour shares the query's component class — not whether it is actually a duplicate. This corpus has no labelled duplicate pairs, so no duplicate metric can be computed directly. Two issues in the same component are usually not duplicates. Treat this as a relative signal for comparing retrieval methods.

method P@5 (all holdout) P@5 (excl. other, n=492) p50 latency p95 latency
embeddings (all-MiniLM-L6-v2, exact cosine) 0.8746 0.2122 4.71 ms 6.02 ms
TF-IDF cosine 0.8877 0.0959 160.65 ms 203.58 ms

The "all holdout" column is dominated by other-matching-other (noise agreeing with noise), which is why TF-IDF looks marginally ahead there. On the slice that means something — the 492 holdout issues that carry a real component label — dense embeddings are 2.2× better than TF-IDF (0.2122 vs 0.0959), at a fraction of the query latency (4.71 ms vs 160.65 ms p50). That gap is the case for embeddings in this project.


Methodology

Time-based split. The holdout is the most recent 20% of issues by creation date; every training issue predates every holdout issue. The boundary is a timestamp, not a row index, so issues sharing the boundary instant all fall on the holdout side — no training issue is contemporaneous with a holdout one. A random split would let the model learn from the future, and on issue trackers that inflates scores badly, because label vocabulary and topics drift week to week. A leakage-guard test asserts the separation, and embedding kNN restricts its candidate pool to the training split via a boolean mask applied before similarity is computed, so a holdout issue can never retrieve itself or a future sibling.

Task construction is a judgment call, made auditable. GitHub labels mix component/area (terminal, git), status (info-needed, duplicate), type (bug, feature-request) and provenance (ai-generated). Only component labels make a meaningful classification target, so the rest are excluded by an explicit stoplist. Classes are the top-K component labels ranked on the training split only; multi-label issues take their most frequent class; everything else collapses to other. Classes with fewer than 10 holdout examples are demoted to other before scoring — a class with two holdout issues produces an F1 that swings wildly on a single prediction and would corrupt the macro average. Here 7 of the 10 requested classes survived; chat-agents-view (416 train / 0 holdout), chat-agent (272/4) and chat-prompts (237/9) were demoted. The complete mapping, stoplist and demotion log are written to class_map.json.

Three methods, weakest first.

  1. Majority baseline — always predict the most frequent training class. The floor that exposes how misleading accuracy is on an imbalanced corpus.
  2. TF-IDF + logistic regression — bag-of-words (1–2 grams) with class_weight="balanced", vectoriser fit on the training split only (fitting on the full corpus would leak holdout vocabulary and IDF weights).
  3. Embedding kNN — cosine top-10 neighbours from the training split, majority vote.

Metrics are computed, never asserted. Everything comes from a run of evals.py and is written to results/<ts>/metrics.json; nothing is hardcoded or carried between runs. The classification quality metrics are deterministic (seed 20260720) and reproduce bit-for-bit; latency is not. classify_component in the server reports the classifier's measured macro-F1, not a self-assessment.


Using it from an LLM client

The server speaks MCP over stdio. It makes no LLM or network calls; every tool returns typed, structured data with the supporting issue numbers.

Tool Returns
corpus_info() Corpus size, date coverage, embedding model, classifier provenance and measured scores
search_similar_issues(query, k=10) Ranked issues: number, title, labels, similarity, snippet
find_duplicates(text, k=10, threshold=0.6) Candidates flagged above/below an (uncalibrated) threshold, with evidence snippets
classify_component(text) Predictions from logreg and kNN, plus the neighbour issues behind the vote
get_issue(number) Stored metadata for one issue
triage(text) Duplicates + classification + every cited issue number, in one object

Registration for Claude Desktop and Claude Code (verified against the current docs): docs/mcp-setup.md. In short, for Claude Code:

claude mcp add --scope local triage-mcp -- uv run python -m triage_mcp.server

Demo — every tool over stdio (make smoke)

The smoke test spawns the server as a subprocess and drives it through the SDK client, the same way a real client does. Abridged real output:

corpus_info
  corpus_size: 47804   repos: ['microsoft/vscode']
  created_from: 2025-07-20 … created_to: 2026-07-20
  embedding_model: sentence-transformers/all-MiniLM-L6-v2   backend: sentence-transformers
  classifier_source_run: 20260720T201357Z   measured_macro_f1: 0.3069562629459519

search_similar_issues(query='terminal hangs during build', k=5)
  #314312  sim=0.6586  Window hang when searching in terminal   [bug, confirmation-pending, terminal-find]
  #265289  sim=0.6545  Terminal is hanging                      [info-needed]
  cited_issue_numbers: [314312, 265289, 265290, 292878, 314080]

classify_component(text=<new terminal-freeze issue>)
  logreg: 'chat-terminal' p=0.8304   (runner-up 'other' p=0.1092)
  kNN:    'other' vote=0.8000
  methods_agree: False          # the two methods disagree — surfaced, not hidden
  measured_quality: macro_f1=0.3070   (from run 20260720T201357Z)

triage(text=<new terminal-freeze issue>)
  duplicates above threshold: 10
  predicted component (logreg): 'chat-terminal'   (kNN): 'other'
  cited_issue_numbers: [259318, 265622, 267001, 268344, 271668, …]
  caveats: 2 attached

That methods_agree: False is the design working as intended: the server hands the client both predictions and the evidence, rather than manufacturing a single confident answer the data does not support.

<!-- Screenshots of the tools running inside Claude Desktop / Claude Code can be added under docs/img/ and linked here. They require a live client session, which is captured locally rather than checked in. -->


Limitations & future work

This is a one-day build, and it is honest about what it is not:

  • No duplicate-pair ground truth. The retrieval metric is a proxy. The right fix is to mine real duplicate pairs from issue timelines — vscode bots post *duplicate and "duplicate of #N" cross-references on close — and score precision/recall/MRR against those, turning find_duplicates from a heuristic into a measured capability.
  • No LLM reranking. Retrieval is a single dense-cosine pass. A cross-encoder or an LLM reranker over the top-k would likely lift precision on the hard cases where lexical and semantic similarity disagree.
  • The client drives the loop manually. A scripted agent loop that calls the tools, validates each response against its Pydantic schema, and emits a structured triage report would make the end-to-end capability testable in CI, independent of a human in a chat client.
  • In-memory exact search. Brute-force cosine over ~48k × 384 is milliseconds and needs no index, which is the right call at this scale. Beyond a few hundred thousand issues, moving the corpus and vectors into Postgres + pgvector would keep it a single dependency while restoring sub-linear search.
  • Single repository. The whole pipeline is repo-parameterised but only vscode is ingested and evaluated. Multi-repo evaluation — does a classifier trained on one repo transfer, or is triage inherently per-repo? — is the natural next experiment.

Some of these limitations are visible in the numbers above: other is 94.8% of the corpus because most vscode issues carry no component label at all (13,026 are entirely unlabelled; another 30,696 have only status/type labels), which caps how high macro-F1 can realistically go on this task.


Project conventions

Type hints throughout; Pydantic for every record that crosses a boundary; unit tests never touch the network (an autouse fixture blocks sockets, and the server integration tests spawn a real subprocess pinned offline). See CLAUDE.md for the full set. Dependencies are pinned in pyproject.toml; make lint runs ruff check and ruff format --check.

License

MIT.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选