Metadata-Version: 2.5
Name: github-mcp-toolkit
Version: 0.1.0
Summary: Standalone, generic MCP server exposing GitHub issues, PRs, commits, and repo metadata.
Project-URL: Repository, https://github.com/421648-1W3-Sanchez-German-Manuel/github-mcp
Author: Manuel Sanchez German
License-Expression: MIT
License-File: LICENSE
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.12
Requires-Dist: libsql-client>=0.3.1
Requires-Dist: mcp>=2.2.0
Requires-Dist: pygithub>=2.10.0
Requires-Dist: python-dotenv>=1.2.3
Requires-Dist: tenacity>=9.1.4
Requires-Dist: uvicorn>=0.52.4
Description-Content-Type: text/markdown

# 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](https://docs.astral.sh/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:

   ```bash
   uv sync
   ```

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

   ```bash
   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:

   ```bash
   uv run pytest
   ```

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

   ```bash
   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](https://turso.tech)
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](https://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):

```bash
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](https://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:

   ```bash
   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:

```bash
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).
