{
  "markdown": "# github-mcp\n\n[![PyPI](https://img.shields.io/pypi/v/jaimenbell-github-mcp)](https://pypi.org/project/jaimenbell-github-mcp/)\n[![MCP Registry](https://img.shields.io/badge/MCP%20Registry-io.github.jaimenbell%2Fgithub--mcp-blue)](https://github.com/jaimenbell/github-mcp/blob/master/server.json)\n[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n[![tests](https://img.shields.io/badge/tests-97%20passing%20%2F%201%20skipped-brightgreen)](#testing)\n[![CI](https://github.com/jaimenbell/github-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/jaimenbell/github-mcp/actions/workflows/ci.yml)\n\nA public read+write MCP server over the GitHub REST API, built to the\n[desktop-mcp](https://github.com/jaimenbell/desktop-mcp)/rag-mcp/[mcp-factory](https://github.com/jaimenbell/mcp-factory) standard (own\npyproject, fastmcp server, honest README, real test suite) with env-gated\ntool groups (**write disabled by default**). **Not the official GitHub MCP\nserver** -- see below.\n\n## Quickstart (60 seconds)\n\n```\npip install jaimenbell-github-mcp\n```\n\n```jsonc\n// Add to your MCP host config (e.g. Claude Desktop/Code's mcpServers block)\n{\n  \"mcpServers\": {\n    \"github-mcp\": {\n      \"command\": \"github-mcp\"\n    }\n  }\n}\n```\n\nThe `write` tool group (issue/PR mutations) is off by default -- see\n[Env vars](#env-vars) below to enable it.\n\n## What this is / is not\n\nThis is a **reference portfolio implementation** demonstrating a hardened\nread+write MCP server pattern over a real external SaaS API (GitHub) --\nenv-gated tool groups, typed error/rate-limit handling, auth that degrades\ngracefully, a real test suite. It exists to show, concretely, \"I build\nread/write MCP servers over external APIs\" with a link a client can click.\n\n**It is NOT the official GitHub MCP server.** It does not aim for parity\nwith GitHub's own MCP offering (GraphQL, Actions, webhooks, GitHub Apps are\nall out of scope -- see below). It started life as a factory-scaffolded\nread-only demo ([mcp-factory](https://github.com/jaimenbell/mcp-factory)'s\n`generated/github_read_server.py`) and was hand-hardened into this\nstandalone read+write server -- the scaffold-then-harden path is itself part\nof the story this repo tells.\n\n## Tools (14)\n\nOne row per tool -- the two groups below just control default-on/off state,\nnot what exists.\n\n| Tool | Group | What it does |\n|---|---|---|\n| `get_repo` | `read` | Repo metadata (stars, language, license, default branch, archived flag...) |\n| `list_issues` | `read` | List issues on a repo (PRs filtered out) |\n| `get_issue` | `read` | Fetch a single issue |\n| `list_pull_requests` | `read` | List pull requests on a repo |\n| `get_pull_request` | `read` | Fetch a single pull request |\n| `get_file_content` | `read` | Read a repo file's content (base64-decoded, binary detected not decoded) |\n| `search_repos` | `read` | Search public repositories |\n| `get_user` | `read` | Public user/org profile |\n| `list_commits` | `read` | List commits on a branch/ref |\n| `create_issue` | `write` | Open an issue |\n| `comment_on_issue` | `write` | Comment on an issue/PR |\n| `update_issue_state` | `write` | Open/close an issue |\n| `add_labels` | `write` | Add labels to an issue/PR |\n| `create_pr_review_comment` | `write` | Create a PR review comment on a diff line |\n\n`read` is always on and works unauthenticated (GitHub's 60 req/hr tier).\n`write` is env-gated and **OFF by default** -- requires\n`GITHUB_MCP_ENABLE_WRITE=1` **and** `GITHUB_TOKEN`.\n\nA disabled write call returns a structured `policy_refusal` error (never a\nsilent no-op, never a crash). A write call with the group enabled but no\ntoken returns a structured `auth_required` error -- the group gate and the\ntoken precondition are checked independently, both before any network call.\n\n## Write-safety-off-by-default\n\nThis is defense-in-depth, mirroring desktop-mcp's `input` group: harness-level\npermission prompts are the first gate, but the server itself refuses every\nwrite tool unless its own environment explicitly opts in with\n`GITHUB_MCP_ENABLE_WRITE=1`, and even then refuses without a `GITHUB_TOKEN`.\nA misconfigured or overly-permissive MCP host cannot turn on GitHub mutations\nthis process wasn't deliberately configured to allow. The registration this\nrepo ships with (see `~/.claude.json`'s `github-mcp` entry) has the write\ngroup **absent from env** -- enabling it is a deliberate per-registration\noperator choice, not a code change.\n\n## Honest-capabilities table\n\nEvery claim below maps to the file that implements it and the test(s) that\nverify it -- no capability is asserted without a corresponding implementation\nand test.\n\n| Claim | Implementation | Verified by |\n|---|---|---|\n| Repo metadata (stars, language, license, default branch, archived flag...) | `github_mcp/groups/read.py::get_repo` | `tests/test_read.py::TestGetRepo`, live: `tests/test_live_smoke.py::test_live_get_repo_real_json` |\n| List / fetch issues (PRs filtered from list) | `github_mcp/groups/read.py::list_issues`, `get_issue` | `tests/test_read.py::TestListIssues`, `TestGetIssue` |\n| List / fetch pull requests | `github_mcp/groups/read.py::list_pull_requests`, `get_pull_request` | `tests/test_read.py::TestListPullRequests`, `TestGetPullRequest` |\n| Read a repo file's content (base64-decoded, binary detected not decoded) | `github_mcp/groups/read.py::get_file_content` | `tests/test_read.py::TestGetFileContent` |\n| Search public repositories | `github_mcp/groups/read.py::search_repos` | `tests/test_read.py::TestSearchRepos` |\n| Public user/org profile | `github_mcp/groups/read.py::get_user` | `tests/test_read.py::TestGetUser` |\n| List commits on a branch/ref | `github_mcp/groups/read.py::list_commits` | `tests/test_read.py::TestListCommits` |\n| Open an issue | `github_mcp/groups/write.py::create_issue` | `tests/test_write.py::TestCreateIssue` |\n| Comment on an issue/PR | `github_mcp/groups/write.py::comment_on_issue` | `tests/test_write.py::TestCommentOnIssue` |\n| Open/close an issue | `github_mcp/groups/write.py::update_issue_state` | `tests/test_write.py::TestUpdateIssueState` |\n| Add labels to an issue/PR | `github_mcp/groups/write.py::add_labels` | `tests/test_write.py::TestAddLabels` |\n| Create a PR review comment on a diff line | `github_mcp/groups/write.py::create_pr_review_comment` | `tests/test_write.py::TestCreatePrReviewComment` |\n| Write group OFF by default, structured refusal when disabled | `github_mcp/config.py::group_enabled`, `gated_write` | `tests/test_config.py::TestGroupEnabled`, `tests/test_write.py::TestGateDisabledByDefault` |\n| Write tools require a token even when the group is enabled | `github_mcp/config.py::check_write_preconditions` | `tests/test_config.py::TestCheckWritePreconditions`, `tests/test_write.py::TestAuthRequiredWhenGroupEnabled` |\n| Fine-grained PAT auth, degrades to unauthenticated tier when absent | `github_mcp/client.py::_headers` | `tests/test_client.py::TestAuthHeaderInjection`, `tests/test_read.py::TestUnauthDegrade` |\n| GitHub primary rate-limit (403 + `X-RateLimit-Reset`) and secondary rate-limit (403 + `Retry-After`, no `X-RateLimit-Remaining`) both surface as a typed error with reset/retry time, never a crash | `github_mcp/client.py::_rate_limit_error`, `_is_rate_limit_response` | `tests/test_client.py::TestRateLimitError`, `tests/test_client.py::TestRateLimitError::test_secondary_rate_limit_no_ratelimit_headers_retry_after_only`, `tests/test_read.py::TestUnauthDegrade::test_get_repo_rate_limited_without_token_is_typed` |\n| Malformed owner/repo/path (control chars etc.) that would raise `httpx.InvalidURL` surfaces as a typed error, never an uncaught exception | `github_mcp/client.py::request` | `tests/test_client.py::TestNetworkError::test_malformed_path_raises_invalid_url_caught_as_network_error` |\n| Generic 4xx/5xx surfaces as a typed error, never a crash | `github_mcp/client.py::_api_error` | `tests/test_client.py::TestApiError` |\n| Non-JSON / malformed responses and network failures surface as typed errors | `github_mcp/client.py::_handle_response`, `request` | `tests/test_client.py::TestDecodeError`, `TestNetworkError` |\n\n## Limitations (read before relying on this)\n\n- **REST v1 only.** No GraphQL API coverage.\n- **No webhooks / GitHub App auth.** Fine-grained PAT only.\n- **No Actions/workflow-dispatch tools.** Issue/PR CRUD is the v1 write surface.\n- **Unauthenticated read is rate-limited to 60 req/hr** by GitHub itself (10\n  req/min for search) -- expect `rate_limited` errors under sustained\n  unauthenticated use; set `GITHUB_TOKEN` (even a read-only fine-grained PAT)\n  to raise this considerably.\n- **`get_file_content` truncates past 100KB** and reports (rather than\n  decodes) non-UTF-8 files.\n- **No pagination beyond a single page** for list endpoints (`limit`, capped\n  per-endpoint, is the only page-size control in v1).\n- **Not registered with the mcp-factory hub.** Ships as a standalone repo\n  (own pyproject, system Python312 install), matching the rag-mcp/desktop-mcp\n  model.\n\n## Env vars\n\n| Var | Effect | Default |\n|---|---|---|\n| `GITHUB_MCP_ENABLE_WRITE` | enable the `write` tool group | unset (off) |\n| `GITHUB_TOKEN` | fine-grained PAT; read works without it (degraded unauth rate), write requires it | unset |\n| `GITHUB_MCP_LIVE` | `1` to run the real-network smoke test (see Testing) | unset (skip) |\n\n## Usage examples\n\n```jsonc\n// A tool call from the MCP host, illustrative -- not a shell command.\n{\"tool\": \"get_repo\", \"arguments\": {\"owner\": \"anthropics\", \"repo\": \"anthropic-sdk-python\"}}\n// -> {\"ok\": true, \"full_name\": \"anthropics/anthropic-sdk-python\", \"stargazers_count\": 1234, ...}\n\n// write group disabled (default):\n{\"tool\": \"create_issue\", \"arguments\": {\"owner\": \"o\", \"repo\": \"r\", \"title\": \"bug\"}}\n// -> {\"ok\": false, \"error\": {\"type\": \"policy_refusal\", \"group\": \"write\", \"required_env\": \"GITHUB_MCP_ENABLE_WRITE\", ...}}\n\n// write group enabled, no token set:\n{\"tool\": \"create_issue\", \"arguments\": {\"owner\": \"o\", \"repo\": \"r\", \"title\": \"bug\"}}\n// -> {\"ok\": false, \"error\": {\"type\": \"auth_required\", \"tool\": \"create_issue\", ...}}\n```\n\n## Testing\n\nCI (`.github/workflows/ci.yml`) runs this suite on every push/PR and fails\nthe build if the Tests badge above drifts from what the suite actually\nreports -- see `scripts/check_readme_counts.py`.\n\n```\n# unit suite (respx-mocked api.github.com, no real network touched)\npython -m pytest -q\n\n# handshake check -- prints every registered tool name\npython scripts/list_tools.py\n\n# real-network read smoke (get_repo against a stable public repo;\n# no write smoke exists anywhere in this suite -- see safety rails above)\nGITHUB_MCP_LIVE=1 python -m pytest -q -k live_get_repo\n```\n\n## Install\n\n```\npip install -r requirements.txt   # or: pip install .\n# deps: fastmcp==3.4.2, httpx==0.28.1\n# test-only: pytest==9.0.3, respx==0.23.1\n```\n\n## Setup / connect\n\n1. `pip install -r requirements.txt` on Python 3.12+.\n2. (Optional) generate a [fine-grained PAT](https://github.com/settings/tokens?type=beta)\n   scoped to the repos you want read+write access to (Issues: read/write,\n   Pull requests: read/write, Contents: read is enough for v1). Read tools\n   work with **no token at all** -- they just run at GitHub's unauthenticated\n   60 req/hr tier.\n3. Add to your MCP host config (e.g. `~/.claude.json`):\n\n```jsonc\n{\n  \"mcpServers\": {\n    \"github-mcp\": {\n      \"command\": \"C:\\\\Users\\\\<you>\\\\AppData\\\\Local\\\\Programs\\\\Python\\\\Python312\\\\python.exe\",\n      \"args\": [\"C:\\\\Users\\\\<you>\\\\projects\\\\github-mcp\\\\run_server.py\"],\n      \"env\": {\n        \"GITHUB_TOKEN\": \"your-fine-grained-pat-here\"\n        // GITHUB_MCP_ENABLE_WRITE intentionally absent -- write stays off\n        // until you deliberately opt in per-deployment.\n      }\n    }\n  }\n}\n```\n\n4. To enable write tools for a given deployment, add\n   `\"GITHUB_MCP_ENABLE_WRITE\": \"1\"` to that entry's `env` block. This is a\n   registration-time operator decision, not a code change.\n\nRegistered in `~/.claude.json` as `github-mcp` (stdio, system Python312,\n`read` group always on, `write` group absent from env -- off).\n\n\n## Commercial support\n\nMaintained by [Jaimen Bell](https://jaimenbell.dev). For production MCP integrations, custom servers, or agent-reliability work, see [jaimenbell.dev](https://jaimenbell.dev).\n\nBuilding your own MCP server? The [MCP Starter Kit](https://jaimenbell.gumroad.com/l/adnojp) has templates, a build playbook, and packaging war-stories from shipping this one.\n\n<!-- MCP registry ownership marker -->\nmcp-name: io.github.jaimenbell/github-mcp\n",
  "bytes": 12518,
  "sha": "5c296d6c88a9c027a1c91fd06fc7d55d038a766ea401313d50f47f9285547aa1",
  "repo_slug": "jaimenbell/github-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_jaimenbell_github_mcp_c74d7412/readme"
}