An MCP server that exposes GitHub issues and pull requests to Claude Desktop and any other MCP-compatible client — built for LLM-driven triage, summarization, and labeling workflows.
- Read-only by default. Write tools (
add_comment,add_label) exist but are only registered when you opt in with--allow-writes. - Projected for models, not dashboards. GitHub's ~40-field issue payload is cut down to the 8 fields a model actually uses for triage. Less noise, far fewer tokens.
- Cursor pagination. List results are capped (default 20, max 50 per call) and continue via an opaque
next_cursor— the model can page without doing page math. - Errors a model can act on. Every failure comes back as structured JSON with a stable
codeand ahintwritten for the LLM: what went wrong and what to do differently.
See DESIGN.md for the reasoning behind these choices.
Requires Node.js ≥ 18.
git clone https://github.com/aaronsomo/github-issues-mcp.git
cd github-issues-mcp
npm install && npm run buildThe build produces dist/index.js — a stdio MCP server that works with any MCP client. Pick yours below.
Add the server to claude_desktop_config.json
(macOS: ~/Library/Application Support/Claude/claude_desktop_config.json,
Windows: %APPDATA%\Claude\claude_desktop_config.json):
{
"mcpServers": {
"github-issues": {
"command": "node",
"args": ["/ABSOLUTE/PATH/TO/github-issues-mcp/dist/index.js"],
"env": {
"GITHUB_TOKEN": "ghp_your_token_here"
}
}
}
}Restart Claude Desktop. You should see the six read tools under the 🔌 MCP indicator. Try:
"List the open issues labeled
bugin facebook/react and summarize the three most recently updated."
To enable writes, add the flag to args — without it the write tools are never registered, so the model cannot call them:
"args": ["/ABSOLUTE/PATH/TO/github-issues-mcp/dist/index.js", "--allow-writes"]Register the server with claude mcp add. Everything after -- is the command Claude Code runs, so server flags like --allow-writes go there:
# read-only
claude mcp add github-issues \
-e GITHUB_TOKEN=ghp_your_token_here \
-- node /ABSOLUTE/PATH/TO/github-issues-mcp/dist/index.js
# with writes (note --allow-writes after the `--`)
claude mcp add github-issues \
-e GITHUB_TOKEN=ghp_your_token_here \
-- node /ABSOLUTE/PATH/TO/github-issues-mcp/dist/index.js --allow-writesThen verify and use it:
claude mcp list # confirms github-issues is connectedInside a session, run /mcp to see the registered tools, then just ask:
"Triage the open issues in vercel/next.js — group the bug reports by area and flag anything updated in the last day."
Scope. claude mcp add defaults to local (this project only). Add -s user to make it available in every project, or -s project to write a checked-in .mcp.json so collaborators get it automatically:
claude mcp add github-issues -s user -e GITHUB_TOKEN=ghp_… -- node /ABSOLUTE/PATH/.../dist/index.jsA -s project registration produces a .mcp.json in the repo root with the same shape as the Claude Desktop snippet above (command / args / env). Don't commit a real token there — use an env var indirection ("GITHUB_TOKEN": "${GITHUB_TOKEN}") and set it in your shell. The first time a collaborator opens the project, Claude Code shows the server as "⏸ Pending approval" until they approve it (claude mcp list reflects this); that's expected for checked-in servers.
Writes & permissions. The write tools carry MCP readOnlyHint: false annotations, so Claude Code surfaces its normal approval prompt before add_comment / add_label run — the --allow-writes gate (which decides whether the tools exist at all) and Claude Code's per-call consent stack together rather than conflict.
To remove or inspect the server later: claude mcp remove github-issues and claude mcp get github-issues.
Set GITHUB_TOKEN (or GH_TOKEN). Without a token the server still works for public repositories, but unauthenticated GitHub API limits are low (60 requests/hour) — a token raises that to 5,000/hour.
| Usage | Fine-grained PAT permission | Classic PAT scope |
|---|---|---|
| Read public repos | none (or any token for the rate limit) | none |
| Read private repos | Issues: Read + Pull requests: Read | repo |
--allow-writes (comment / label) |
Issues: Read and write | public_repo (public) / repo (private) |
public_repois a write scope — don't grant it for read-only use; reading public repos needs no scope at all.
Use the smallest scope that covers your use — there is no reason to hand a read-only triage assistant a write-capable token.
| Tool | What it returns |
|---|---|
list_issues |
Issue summaries for a repo (PRs filtered out) — filter by state, labels, assignee, since |
get_issue |
One issue in detail: body, assignees, milestone, lifecycle dates |
list_issue_comments |
The discussion thread of an issue or PR |
list_pull_requests |
PR summaries — state reports open/closed/merged |
get_pull_request |
One PR in detail: branches, merge status, diff stats |
search_issues |
Full GitHub search syntax across issues and PRs (repo:, label:, is:, ...) |
Each list_issues summary row is exactly eight fields:
{
"number": 4096,
"title": "Crash when window is resized during render",
"state": "open",
"author": "octocat",
"labels": ["bug", "p1"],
"comments": 7,
"updated_at": "2026-06-01T12:00:00Z",
"url": "https://github.com/facebook/react/issues/4096"
}list_pull_requests rows have the same shape but carry draft (boolean) in place of comments, and state reports open/closed/merged. search_issues rows add a ninth field, type (issue or pull_request), so the model can tell them apart in a mixed result set.
List results include next_cursor when more pages exist — pass it back verbatim to continue.
| Tool | What it does |
|---|---|
add_comment |
Posts a comment on an issue or PR |
add_label |
Adds labels to an issue or PR (existing labels are kept; labels must already exist in the repo) |
Both writes are additive — nothing in this server can close, edit, or delete anything.
Failures return structured, model-readable JSON, e.g.:
{
"error": {
"code": "rate_limited",
"status": 403,
"message": "API rate limit exceeded",
"hint": "GitHub rate limit hit. The rate limit resets at 2026-06-10T21:30:00.000Z. Authenticated requests get a much higher limit — set GITHUB_TOKEN if it is missing.",
"retry_after_seconds": 1200
}
}Codes: unauthorized, forbidden, rate_limited, not_found, issues_disabled, validation_failed, invalid_cursor, invalid_response, network_error, timeout, cancelled, server_error, http_error, internal_error.
One exception to the structured shape: arguments that fail the tool's input schema (e.g. limit above 50, or a malformed owner/repo) are rejected by the MCP SDK before the handler runs, so they come back as the SDK's own -32602 plain-text error rather than the {error:{...}} payload. Every error originating inside the server uses the structured form.
npm test # vitest suite (no network — fetch is injected)
npm run typecheck # strict TS, no emit
npm run build # compile to dist/