Skip to main content

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:

  1. Taiga MCP — already built.
  2. GitHub MCP (this project).
  3. 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

  1. Install dependencies:

    uv sync
    
  2. Copy the example env file and fill in your token:

    cp .env.example .env
    
    • GITHUB_TOKEN: your Personal Access Token.
    • GITHUB_REPOS: comma-separated owner/repo allow-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.
  3. Verify the setup:

    uv run pytest
    
  4. Register the server with Claude Code (adjust the path):

    claude mcp add github-mcp -- uv run --directory /path/to/github-mcp github-mcp
    
  5. Restart Claude Code, then ask it to call the github_ping tool 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. author and labels are 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) — includes percent_closed per milestone.
  • list_labels(repo).
  • search_issues_and_prs(query, repo, limit) — free-text search (GitHub search qualifiers supported in query); scoped to repo, or to all of GITHUB_REPOS if 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)labels replaces the list entirely (omit to leave unchanged, [] to clear); milestone takes 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", optional state_reason: "completed"/"not_planned") or reopen (state="open") an issue.
  • create_milestone(repo, title, due_on, description, confirm) and update_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:

  1. 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.
  2. The webhook receiver (src/github_mcp/webhook/): a small, separate Starlette/uvicorn HTTP process — not the stdio MCP server — that GitHub pushes issues/pull_request/push/milestone events 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 at https://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

  1. Go to turso.tech and sign up (GitHub login works).
  2. Create a database (any name, e.g. github-mcp-cache).
  3. From its dashboard, copy the connection URL (libsql://...turso.io) and generate/copy an auth token.
  4. Put both in your local .env as TURSO_DATABASE_URL and TURSO_AUTH_TOKEN — this lets you run sync_repo locally 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

  1. On render.com, inside a project: New +Web Service.

  2. Connect the GitHub repo from step 2, branch main.

  3. Settings:

    • Runtime: Python 3
    • Build Command: pip install uv && uv sync --frozen
    • Start Command: uv run github-mcp-webhook
    • Instance Type: Free
  4. 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 PORT itself; don't add it.
  5. 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):

  1. Repo → Settings → Webhooks → Add webhook.
  2. Payload URL: https://<your-app>.onrender.com/webhook
  3. Content type: application/json (recommended — form also works, see caveat below, but JSON is simpler).
  4. Secret: the exact same GITHUB_WEBHOOK_SECRET from step 3.
  5. Which events: "Let me select individual events" → check Issues, Pull requests, Pushes, Milestones.
  6. Save. GitHub immediately sends a ping event — 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_ping tool.
  • 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_cached tools 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=True and 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

github_mcp_toolkit-0.1.0.tar.gz (144.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

github_mcp_toolkit-0.1.0-py3-none-any.whl (34.6 kB view details)

Uploaded Python 3

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

Hashes for github_mcp_toolkit-0.1.0.tar.gz
Algorithm Hash digest
SHA256 cf5a9f4e4b498fb1749c977e4bd226aa9b6585f7236f88646bfa2c012c3189ab
MD5 b56defb7c17801d5594da1d6e70cd77a
BLAKE2b-256 364670bf6621bda1a07f5c586a49ea18de38daad83234f8ec66d16bb8550600d

See more details on using hashes here.

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

Hashes for github_mcp_toolkit-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8a11649b192d3af7bbfc3a7534a9a23618f965d40322285b1a94a0567486b9de
MD5 05f76af566198422dc8e21e6afd1fa5e
BLAKE2b-256 8cea5552c0d807d7d341bfc973c4a86a1d2dfa1aa7a7cbf18c6bbefcba8b056f

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page