Skip to main content
Glama
JigeeshaJain

gh-review-queue-mcp

by JigeeshaJain

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.

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.


Related MCP server: github-ops-mcp

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.

F
license - not found
Not graded
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

View all related MCP servers

Related MCP Connectors

  • A Model Context Protocol (MCP) application for automated GitHub PR analysis and issue management.…

  • An MCP server that gives your AI access to the source code and docs of all public github repos

  • A MCP server built for developers enabling Git based project management with project and personal…

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/JigeeshaJain/ReviewQueueMcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server