GitHub Read MCP Server
by jaimenbell
README.md
# github-read — read-only GitHub MCP server
A production-grade, **read-only** MCP server for the GitHub REST API. Wire it
into Claude Desktop / Claude Code / Cursor and the agent can answer questions
about repositories, issues, files, and users — without any write access.
> Scaffolded by [mcp-factory](https://github.com/jaimenbell/mcp-factory); the
> value is the production layer on top: auth-scoping, fail-soft, version-pinning,
> tests, docs.
## Tools
| Tool | What it does |
|---|---|
| `get_repo` | Repo metadata: stars, language, open issues, default branch, license, last push |
| `list_issues` | Open/closed/all issues (PRs filtered out), newest-update first |
| `get_file_contents` | Read a UTF-8 text file at a path (truncates past ~100 KB) |
| `search_repositories` | Search public repos by keyword/qualifiers |
| `get_user` | Public profile for a user or org |
## Install
```bash
cd github-read-mcp
python -m venv .venv
.venv/Scripts/python.exe -m pip install -r requirements.txt # Windows
# source .venv/bin/activate && pip install -r requirements.txt # macOS/Linux
```
Dependencies are **version-pinned** (`requirements.txt`); a fresh install
reproduces the tested build.
## Authentication & scoping (read this)
The token is **optional**:
- **No token** → the server runs on GitHub's **unauthenticated 60 req/hr** tier.
It works, but rate-limits quickly. It prints a one-line warning to stderr.
- **With a token** → **5000 req/hr**. Use a **fine-grained personal access
token** scoped to **read-only**:
- *Repository access*: only the repos the agent should see (or "public repos").
- *Permissions*: **Contents: Read-only** and **Metadata: Read-only**. Nothing else.
This server only ever issues HTTP **GET**s — there is no tool that can write,
delete, or mutate anything. Even if handed a broader token, it cannot take a
destructive action. Grant the minimum anyway; least privilege is the point.
Set the token via the `GITHUB_TOKEN` env var (see registration below). **Never
commit a token.**
## Register in Claude / Cursor
Add to your MCP config (`~/.claude.json` under `mcpServers`, or Cursor's
`mcp.json`):
```json
{
"mcpServers": {
"github-read": {
"command": "C:\\path\\to\\github-read-mcp\\.venv\\Scripts\\python.exe",
"args": ["-m", "github_mcp.server"],
"env": { "GITHUB_TOKEN": "github_pat_xxx_your_readonly_token" }
}
}
}
```
The `command` must be the venv interpreter (so the pinned deps are on the path),
and the working directory must be the repo root so `-m github_mcp.server`
resolves. Leave `GITHUB_TOKEN` as `""` to run unauthenticated. Restart the client
to pick up the server.
## Verify it works
```bash
# Hermetic unit/integration suite (no network, no token):
.venv/Scripts/python.exe -m pytest -q
# Live tests against the real unauthenticated GitHub API:
RUN_LIVE_GITHUB_TESTS=1 .venv/Scripts/python.exe -m pytest tests/test_live.py -v
# End-to-end MCP protocol smoke (spawns the server, drives it as a client would):
.venv/Scripts/python.exe scripts/smoke_mcp.py
```
## Fail-soft behavior
Every failure mode returns a structured JSON envelope — a tool call **never**
crashes the agent session:
```json
{ "error": { "type": "rate_limited",
"message": "GitHub rate limit exhausted on the unauthenticated (60/hr) tier.",
"hint": "Set a GITHUB_TOKEN to raise the limit to 5000 req/hr.",
"retry_after_seconds": 118.4 } }
```
Handled: `rate_limited`, `secondary_rate_limited`, `unauthorized`, `forbidden`,
`not_found`, `invalid_request`, `is_directory`, `binary_file`, `timeout`,
`network_error`, `bad_response`, `internal_error`. A **missing** required
argument is rejected by the MCP SDK itself (protocol-level `isError`) before it
reaches the server.
## Layout
```
github-read-mcp/
├── mcp.yaml # factory manifest (the deliverable spec)
├── github_mcp/
│ ├── server.py # MCP server: tools + fail-soft boundary
│ ├── client.py # GitHub client: auth, version-pin, fail-soft
│ ├── errors.py # error normalization
│ └── _scaffold_generated.py # original factory stub (provenance / before-after)
├── tests/ # 31 hermetic + 4 live (network-gated)
├── scripts/smoke_mcp.py # end-to-end MCP protocol smoke
├── requirements.txt # pinned runtime deps
└── requirements-dev.txt # pinned test deps
```
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues