github-mcp
A standalone, generic MCP server exposing GitHub issues, pull requests, commits, and repo metadata as tools.
Architecture
This is one of three independent components in a larger system:
- Taiga MCP — already built.
- GitHub MCP (this project).
- Fusion/Correlation MCP — consumes the two servers above (never the raw Taiga/GitHub APIs directly) to correlate stories, tasks, issues, PRs, and commits.
This server is self-contained and generic: it is useful even without Taiga present, and it has no knowledge of Taiga or of any correlation logic — that responsibility lives entirely in the Fusion/Correlation MCP.
Requirements
- Python 3.12+
- uv
- A GitHub Personal Access Token (fine-grained, recommended scopes to start:
Contents: read,Issues: read,Pull requests: read,Metadata: read)
Setup
-
Install dependencies:
uv sync -
Copy the example env file and fill in your token:
cp .env.example .env
GITHUB_TOKEN: your Personal Access Token.GITHUB_REPOS: comma-separatedowner/repoallow-list (e.g.acme/backend,acme/frontend). Leave empty to allow any repo the token can access.GITHUB_API_URL: only set this for GitHub Enterprise Server.
-
Verify the setup:
uv run pytest
-
Register the server with Claude Code (adjust the path):
claude mcp add github-mcp -- uv run --directory /path/to/github-mcp github-mcp
-
Restart Claude Code, then ask it to call the
github_pingtool to confirm the token and rate limit are reachable.
Tools
github_ping— verifies the token and reports rate limit status.list_issues(repo, state, labels, milestone, assignee, since, limit)— lists issues (pull requests excluded).get_issue(repo, number)— issue detail: body, comments, sub-issues.list_pull_requests(repo, state, author, base, head, labels, limit)— lists PRs.authorandlabelsare applied client-side (GitHub's list-pulls endpoint doesn't support filtering by them).get_pull_request(repo, number)— PR detail: commits, reviews, CI checks and combined status for the head commit.list_commits(repo, branch, since, until, author, limit).list_milestones(repo, state)— includespercent_closedper milestone.list_labels(repo).search_issues_and_prs(query, repo, limit)— free-text search (GitHub search qualifiers supported inquery); scoped torepo, or to all ofGITHUB_REPOSif omitted.extract_external_refs(text, pattern)— pulls external tracker references (e.g. "TG-123") out of a title/body/commit message. Pure text utility, no GitHub API call.find_closing_keywords(text)— finds GitHub closing keywords (Closes/Fixes/Resolves#N) in a piece of text and which issue each references. Pure text utility.find_references_to_issue(repo, number)— uses GitHub's own issue timeline to return every PR/issue/commit that references a given issue (via closing keyword or plain mention) — this is what GitHub itself considers a link, not a text-search guess.sync_repo(repo, entity_types, limit)— fetches issues/PRs/commits/ milestones/labels live and upserts them into the local cache. Use this to warm the cache, or to catch up if the webhook receiver (below) isn't running or missed events.get_cache_status(repo)— per-entity-type counts and last-synced time in the cache.list_cached(repo, entity_type)/get_cached(repo, entity_type, key)— read back what's cached, without another live API call.create_issue(repo, title, body, labels, assignees, confirm).update_issue(repo, number, labels, milestone, confirm)—labelsreplaces the list entirely (omit to leave unchanged,[]to clear);milestonetakes a milestone number as a string, or"none"to clear.add_comment(repo, number, body, confirm).set_issue_state(repo, number, state, state_reason, confirm)— close (state="closed", optionalstate_reason:"completed"/"not_planned") or reopen (state="open") an issue.create_milestone(repo, title, due_on, description, confirm)andupdate_milestone(repo, number, title, state, due_on, description, confirm)— e.g. mapped to a Taiga Sprint.
All repo-scoped tools take repo as "owner/repo" and enforce the
GITHUB_REPOS allow-list from .env when one is set.
Write tools require confirm=True. Without it, they change nothing and
return a preview of what they would do instead — deliberate friction on top
of whatever tool-approval prompting the MCP client itself already does,
since these actions (a new issue, a comment, a closed issue) are visible to
the whole team. Writing to issues also needs the token's Issues
repository permission bumped from Read-only to Read and write.
Local cache and webhooks
Tool calls always hit the live GitHub API — the cache is a separate, opt-in store for a consumer (chiefly a future correlation MCP) that needs to query the same repo state repeatedly without repeating live calls. Two ways to fill it:
sync_repo(an MCP tool): pulls a fresh snapshot on demand and upserts it into the cache. Works with zero extra setup, local SQLite is fine.- The webhook receiver (
src/github_mcp/webhook/): a small, separate Starlette/uvicorn HTTP process — not the stdio MCP server — that GitHub pushesissues/pull_request/push/milestoneevents to, keeping the same cache warm in near real time. This one needs a public HTTPS endpoint and a hosted database (see below); it's currently deployed for this project athttps://github-mcp-k49a.onrender.com.
Cache backend: local SQLite or Turso
By default the cache is a local SQLite file (data/cache.sqlite3,
configurable via GITHUB_MCP_DB_PATH) — fine for running this MCP on your
own machine. Deployed somewhere with an ephemeral filesystem (Render's free
tier included), that file gets wiped on every restart/redeploy, so the
webhook receiver needs a real database instead: set TURSO_DATABASE_URL
(and TURSO_AUTH_TOKEN) to point it at a Turso
database — same SQL, data lives outside the process. When set, it takes
priority over GITHUB_MCP_DB_PATH. Both the MCP server and the webhook
receiver must point at the same Turso database to actually share one
cache.
Turso's dashboard hands out a libsql:// URL; paste it exactly as given —
this MCP normalizes it to https:// internally, because in some
sandboxed/proxied network environments the libsql:// websocket handshake
fails while the same database's plain HTTP endpoint works fine (found by
testing against a real Turso database).
Deploying the webhook receiver from scratch
Everything below is one-time setup per environment. All of it is already done for this project (see the live URL above) — follow this if you're setting it up again (new Turso DB, new Render account, a fork, etc).
1. Create a Turso database
- Go to turso.tech and sign up (GitHub login works).
- Create a database (any name, e.g.
github-mcp-cache). - From its dashboard, copy the connection URL (
libsql://...turso.io) and generate/copy an auth token. - Put both in your local
.envasTURSO_DATABASE_URLandTURSO_AUTH_TOKEN— this lets you runsync_repolocally against the same database the deployed webhook receiver will use.
2. Push this repo to GitHub (skip if already done):
git remote add origin https://github.com/<you>/<repo>.git
git push -u origin main
3. Create a Render Web Service
-
On render.com, inside a project: New + → Web Service.
-
Connect the GitHub repo from step 2, branch
main. -
Settings:
- Runtime: Python 3
- Build Command:
pip install uv && uv sync --frozen - Start Command:
uv run github-mcp-webhook - Instance Type: Free
-
Environment Variables (add before deploying):
GITHUB_WEBHOOK_SECRET— any secret string you choose (e.g.python3 -c "import secrets; print(secrets.token_hex(32))"). You'll reuse this exact value in step 4.TURSO_DATABASE_URL/TURSO_AUTH_TOKEN— same values as your.env.- Render sets
PORTitself; don't add it.
-
Click Deploy Web Service. You'll get a public URL like
https://<something>.onrender.com. Confirm it's up:curl https://<your-app>.onrender.com/healthz # -> {"ok":true}
4. Configure the GitHub webhook, per repo
For each repo you want live-syncing (repeat this whole step per repo):
- Repo → Settings → Webhooks → Add webhook.
- Payload URL:
https://<your-app>.onrender.com/webhook - Content type:
application/json(recommended —formalso works, see caveat below, but JSON is simpler). - Secret: the exact same
GITHUB_WEBHOOK_SECRETfrom step 3. - Which events: "Let me select individual events" → check
Issues,Pull requests,Pushes,Milestones. - Save. GitHub immediately sends a
pingevent — check the webhook's Recent Deliveries tab for a green ✅ (200 response). If it's red, click it to see the response body, or use Redeliver to retry after fixing something.
5. Verify it's actually populating the cache
Trigger a real event (open an issue, for instance), then check from your machine:
uv run python -c "
from github_mcp.config import load_config
from github_mcp.cache import Cache
config = load_config()
cache = Cache(config.db_path, turso_url=config.turso_url, turso_auth_token=config.turso_auth_token)
print(cache.status('owner/repo'))
"
You should see a non-empty count for issue (or whichever entity you
triggered) with a recent last_synced_at.
Caveat — GitHub's "form" content type: if a webhook is configured with
Content type form instead of application/json, GitHub wraps the JSON
payload inside a payload form field instead of sending it raw. This MCP
handles both (found and fixed by testing against the real deployed
service — see webhook/app.py), so either setting works; JSON is just more
standard.
Naming convention for correlation
To let a correlation layer (or a human) match a GitHub issue/PR/commit back
to its Taiga item, include the Taiga item's Ref number prefixed with
TG- somewhere in the GitHub title or body — e.g. a Taiga user story with
Ref #123 becomes TG-123 in the corresponding GitHub issue/PR title:
TG-123 · Add password reset endpoint
extract_external_refs looks for exactly this shape by default (one or more
uppercase letters, a hyphen, digits — so it also works unmodified for
JIRA-123-style keys, if that's ever needed instead). This is a convention
this MCP expects, not one it enforces — adopting it consistently is on
whoever writes the issue/PR titles.
Project layout
src/github_mcp/
config.py # env/config loading (GITHUB_TOKEN, GITHUB_REPOS, ...)
errors.py # standardized error types + GithubException translation
client.py # GitHubClient: retries, backoff, pagination, repo allow-list
cache.py # cache (repo, entity_type, entity_key) -> JSON; SQLite or Turso backend
app_context.py # shared lifespan state (client, cache) injected into tools
server.py # MCPServer entrypoint and lifespan wiring (stdio)
tools/
serialize.py # PyGithub object -> plain dict conversion
params.py # shared arg-parsing helpers (date parsing, etc.)
issues.py, pull_requests.py, commits.py, milestones.py, labels.py,
search.py, references.py, sync.py, mutations.py, health.py
webhook/ # standalone process, not part of the stdio MCP server
app.py # Starlette app + `github-mcp-webhook` entrypoint
security.py # X-Hub-Signature-256 verification
transform.py # webhook JSON payload -> same shape as serialize.py
tests/
Status
- Fase 0 — Foundational/setup: done. Auth (PAT), base client with retries
and pagination helpers, standardized error handling, and project structure
are in place, verified with unit tests and the
github_pingtool. - Fase 1 — Read tools: done. Issues, PRs, commits, milestones, labels, and search, all verified against real repos and covered by mocked unit tests.
- Fase 2 — Correlation-support utilities: done. Reference extraction, closing-keyword detection, and issue cross-reference lookup, verified against real repos and covered by unit tests. Naming convention documented above.
- Fase 3 — Webhooks and persistence: done and deployed. Cache (SQLite
locally, or hosted Turso in production) and
sync_repo/get_cache_status/list_cached/get_cachedtools are live and verified against real repos. The webhook receiver is deployed on Render (https://github-mcp-k49a.onrender.com), connected to a real Turso database, and confirmed end to end with a real GitHub webhook delivery landing in the cache. See "Local cache and webhooks" above for the full setup steps (Turso, Render, GitHub webhook config) and the redeploy steps if this ever needs to be set up again elsewhere. - Fase 4 — Bidirectional actions: done. create_issue, update_issue,
add_comment, set_issue_state, create_milestone, and update_milestone, all
gated behind
confirm=Trueand verified end to end against a real repo (create → comment → update labels → close, and create/close a milestone).
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file github_mcp_toolkit-0.1.0.tar.gz.
File metadata
- Download URL: github_mcp_toolkit-0.1.0.tar.gz
- Upload date:
- Size: 144.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cf5a9f4e4b498fb1749c977e4bd226aa9b6585f7236f88646bfa2c012c3189ab
|
|
| MD5 |
b56defb7c17801d5594da1d6e70cd77a
|
|
| BLAKE2b-256 |
364670bf6621bda1a07f5c586a49ea18de38daad83234f8ec66d16bb8550600d
|
File details
Details for the file github_mcp_toolkit-0.1.0-py3-none-any.whl.
File metadata
- Download URL: github_mcp_toolkit-0.1.0-py3-none-any.whl
- Upload date:
- Size: 34.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8a11649b192d3af7bbfc3a7534a9a23618f965d40322285b1a94a0567486b9de
|
|
| MD5 |
05f76af566198422dc8e21e6afd1fa5e
|
|
| BLAKE2b-256 |
8cea5552c0d807d7d341bfc973c4a86a1d2dfa1aa7a7cbf18c6bbefcba8b056f
|