gh-review-queue-mcp

gh-review-queue-mcp

MCP server that ranks and deduplicates your GitHub pull request review queue across direct requests, team requests, and your own waiting PRs into a single prioritized list via one get_review_queue tool.

Category
访问服务器

README

gh-review-queue-mcp

An MCP server that answers one question: what should I review next?

It exposes exactly one tool, get_review_queue, which returns a ranked, deduplicated view of your GitHub pull request review queue — reviews requested of you, reviews requested of your teams, and your own pull requests that are waiting on someone else.

One tool is a deliberate constraint. An assistant that has to pick between list_prs, search_prs, and get_pr_status spends its first turn choosing; an assistant with one tool that returns an already-prioritized list can just answer.


What it actually does

When the tool is called, four things happen in order.

1. Identify you and your teams

The server issues a GraphQL query for viewer { login } plus the teams you belong to (organizations.teams(role: MEMBER)). The team slugs matter because GitHub's search API has no "requested of any of my teams" qualifier — you have to name each team explicitly. This is the only reason the token needs the read:org scope.

2. Fan out into one batched search

GitHub has no single query for "everything needing my attention", so the server runs several searches and combines them. All of them go out in one GraphQL document using aliases, so it is one HTTP round trip regardless of how many teams you're on:

Alias Search Becomes reason
requested_of_me is:pr is:open archived:false review-requested:@me requested_of_me
my_pr_awaiting_review is:pr is:open archived:false author:@me my_pr_awaiting_review
team_0, team_1, … is:pr is:open archived:false team-review-requested:<org>/<team> requested_of_my_teams

Search strings are passed as GraphQL variables, never interpolated into the query document, so a team slug can't reshape the query.

The same query also asks for rateLimit { remaining resetAt }, so every response can report your remaining budget without a second call.

Two notes on the response shape. GitHub's search(type: ISSUE) returns issues as well as pull requests; because the selection set is an inline fragment on PullRequest, issues come back as empty nodes and are dropped during parsing. And statusCheckRollup is read from commits(last: 1) — the CI state of the head commit, not the whole branch history.

3. Merge, dedupe, filter, rank

The same pull request routinely comes back from several searches — a PR where you're a direct reviewer and your team is requested appears in two buckets. They're deduplicated on GraphQL node id, and the reasons accumulate onto one entry, so the response says "this is here for two reasons" instead of listing it twice.

Then your filters are applied, and what survives is scored and sorted.

4. Serialize

The ranked list comes back as structured output — the tool declares a full JSON output schema, so a client gets typed fields, not prose it has to parse.


How ranking works

Ranking is tiered, not weight-tuned. Each pull request lands in exactly one tier, and the tier is worth vastly more than anything that accumulates inside one:

Tier Condition Base
3 Your own PR with failing CI 300
2 Your own PR with changes requested 200
1 A review requested of you directly 100
0 A team request, or your own PR that's simply waiting 0

Within a tier, two smaller signals apply:

  • Age — 2 points per day since the PR was opened, capped at 20. Old review requests surface, but a six-month-old PR can't dominate forever.
  • Small diff — a flat 8-point bonus for diffs of 100 lines or fewer, on the theory that a small review you can finish now beats a large one you'll defer.

The cap is the whole point. The most anything can accumulate inside a tier is 20 + 8 = 28, well under the tier step of 100, so tier dominance holds by construction: a brand-new direct request always outranks an ancient team request, and no future weight tuning can silently flip that. If you add a scoring signal, keep the within-tier total under 100 or that guarantee breaks.

Ties break on most recent activity (updatedAt), so an active discussion outranks a stalled one at the same score.

Every item carries priority_reasons — human-readable strings like ["my PR, CI failing", "3 days old"] — so the ranking can be explained back to you instead of arriving as an unexplained number.


Installation

Requires Python 3.11+ and uv.

git clone <this repo>
cd ReviewQueueMcp
uv sync

Token

The server reads a GitHub personal access token from GITHUB_TOKEN:

cp .env.example .env      # then edit it
export GITHUB_TOKEN=ghp_...

Scopes needed:

  • repo — read pull requests in private repositories
  • read:org — read your team memberships, for the team-review-requested searches

A classic PAT is simplest. Fine-grained tokens work if granted "Pull requests: read" plus organization member read. Create one at https://github.com/settings/tokens.

GITHUB_GRAPHQL_URL optionally overrides the endpoint for GitHub Enterprise Server.

The token is read per tool call, not at startup — the server starts cleanly without one and returns an actionable error when called, rather than dying during the MCP handshake where the client would only see a broken pipe.


Running it

uv run gh-review-queue-mcp

It speaks MCP over stdio and expects a client on the other end; run directly, it just waits.

With MCP Inspector

npx @modelcontextprotocol/inspector uv --directory /absolute/path/to/ReviewQueueMcp run gh-review-queue-mcp

Open the printed URL, connect, and the tool appears under Tools with its generated input schema.

With Claude Desktop

Add to claude_desktop_config.json — on macOS at ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "gh-review-queue": {
      "command": "uv",
      "args": [
        "--directory",
        "/absolute/path/to/ReviewQueueMcp",
        "run",
        "gh-review-queue-mcp"
      ],
      "env": {
        "GITHUB_TOKEN": "ghp_..."
      }
    }
  }
}

Paths must be absolute — Claude Desktop doesn't launch servers from your shell, so it has no working directory or exported environment to inherit. Restart Claude Desktop after editing. Then ask it "what should I review today?"


Tool reference

get_review_queue

All arguments are optional.

Argument Type Default Meaning
include array of requested_of_me | requested_of_my_teams | my_pr_awaiting_review all three Which reasons to include. An item survives if any of its reasons is included.
exclude_drafts boolean true Drop drafts. They're excluded, not demoted — a draft isn't reviewable yet.
max_age_days integer none Drop PRs opened more than this many days ago. Inclusive at the boundary.
repos array of owner/name none Restrict to these repositories. Exact match.
limit integer 1–100 25 Maximum items returned. total_matching still reports the full count.

Response:

{
  "viewer": "octocat",
  "generated_at": "2026-08-20T12:00:00Z",
  "returned": 5,
  "total_matching": 5,
  "rate_limit_remaining": 4712,
  "warnings": [],
  "items": [
    {
      "repository": "acme/payments-api",
      "number": 4830,
      "title": "Add idempotency keys",
      "url": "https://github.com/acme/payments-api/pull/4830",
      "author": "octocat",
      "reasons": ["my_pr_awaiting_review"],
      "priority_score": 306.0,
      "priority_reasons": ["my PR, CI failing", "3 days old"],
      "age_days": 3.0,
      "diff_size": 374,
      "changed_files": 12,
      "is_draft": false,
      "review_decision": "REVIEW_REQUIRED",
      "ci_status": "FAILURE"
    }
  ]
}

returned vs total_matching distinguishes "here are 25" from "there are numerous" — without it, a limited response is indistinguishable from a complete one.

warnings carries GraphQL partial failures. GitHub can return usable data alongside errors (one org unreadable, one search failing); rather than throwing away the whole queue, those degrade to warnings and the rest of the results still come back.


Architecture

Four modules under src/gh_review_queue/, and the boundaries are load-bearing:

server.py    MCP wiring. Parse arguments -> call client -> domain layer -> serialize.
   |         Deliberately thin; its docstring sets a ~120-line budget.
   v
github.py    The only module that touches the network. Builds GraphQL, handles HTTP
   |         and GraphQL errors, returns domain objects. Never ranks or filters.
   v
queue.py     Pure functions: merge -> apply_filters -> rank/score, via build_queue.
   |         Input is a snapshot and a clock. Nothing else.
   v
models.py    Frozen pydantic value objects. The only place GitHub's nested GraphQL
             shape is flattened. No network types.

The payoff is queue.py: because it takes a QueueSnapshot and a datetime and nothing else, every ranking rule is tested with plain data and no mocks, no network, and no clock patching. That's the reason for the split, and why an httpx import must never reach it.

Degrading instead of failing

Unknown enum values from GitHub — a new reviewDecision, a new CI rollup state — are mapped to None rather than raising. A state added on GitHub's side should never break your whole queue. The same instinct runs through the parsing layer: missing authors become ghost (GitHub's own convention for deleted accounts), non-PR search results are dropped, and absent timestamps are the one genuinely unrecoverable case that does raise.


Development

uv run pytest                       # all tests
uv run pytest tests/test_queue.py   # one file
uv run pytest -k "rank or score"    # by name
uv run ruff check .                 # lint
uv run ruff format .                # format
uv run mypy                         # typecheck (strict)

Run mypy bare — it takes its targets from [tool.mypy] files in pyproject.toml, so passing a path checks less than intended.

Testing approach

Tests run off tests/fixtures/queue_response.json, one captured GraphQL response built to contain the awkward cases: a PR that appears in two buckets, a draft, a very stale PR, a failing-CI PR of the viewer's, and a null status rollup.

test_rank_orders_the_fixture_the_way_a_reviewer_would_read_it asserts exact scores against a fixed clock. It's the canary for scoring changes — if it fails, decide whether the new ordering is genuinely better before updating the numbers.


Status

Phase Scope State
1 Scaffold, packaging, tooling done
2 models.py, queue.py, domain tests done
3 github.py GraphQL client, real server.py done
4 Client and server tests not started
5 Documentation this file

Phase 3 is verified end to end — a real MCP stdio handshake, tool discovery, and a tool call — but tests/test_server.py is still a placeholder. The client's error paths (401, 403, partial GraphQL failures, unreachable host) are written but not yet covered by automated tests.

推荐服务器

Baidu Map

Baidu Map

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

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

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

官方
精选
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

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

官方
精选
本地
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

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

官方
精选
本地
TypeScript
VeyraX

VeyraX

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

官方
精选
本地
Kagi MCP Server

Kagi MCP Server

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

官方
精选
Python
graphlit-mcp-server

graphlit-mcp-server

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

官方
精选
TypeScript
mcp-server-qdrant

mcp-server-qdrant

这个仓库展示了如何为向量搜索引擎 Qdrant 创建一个 MCP (Managed Control Plane) 服务器的示例。

官方
精选
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

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

官方
精选