|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
| 4 | + |
| 5 | +## Project Overview |
| 6 | + |
| 7 | +MCP server providing GitHub integration for LLMs. Exposes tools for PR analysis, issue management, tags/releases, user search/activity, and IP info. Runs in stdio mode (IDE integration) or HTTP mode (remote access). |
| 8 | + |
| 9 | +## Architecture |
| 10 | + |
| 11 | +``` |
| 12 | +MCP Client (IDE/LLM) |
| 13 | + | |
| 14 | + | stdio or HTTP (streamable-http) |
| 15 | + v |
| 16 | +PRIssueAnalyser (FastMCP) -> src/mcp_github/issues_pr_analyser.py |
| 17 | + | |
| 18 | + | calls |
| 19 | + v |
| 20 | +GitHubIntegration -> src/mcp_github/github_integration.py |
| 21 | + |-> REST API (httpx) -> GitHub REST API v3 |
| 22 | + |-> GraphQL API (GraphQLClient) -> GitHub GraphQL v4 |
| 23 | +IPIntegration -> src/mcp_github/ip_integration.py |
| 24 | + |-> REST API (httpx) -> ipinfo.io |
| 25 | +``` |
| 26 | + |
| 27 | +**Tool Registration**: `_register_tools()` calls `register_tools()` on both `GitHubIntegration` and `IPIntegration` instances. `register_tools()` uses `inspect.getmembers()` to find all public methods and registers each as an MCP tool via `mcp.add_tool()`. **To add a new tool, add a public method with type-annotated parameters to either class** — it auto-registers. |
| 28 | + |
| 29 | +**Python 3.14 compatibility**: Tool registration uses `inspect.isroutine()` (not `ismethod()` or `isfunction()`) because Python 3.14 changed bound method detection — `inspect.ismethod()` returns `False` for methods accessed through class instances. |
| 30 | + |
| 31 | +**ResponseCachingMiddleware removed**: Was causing `tools/list` to fail with "TTL is invalid" error in FastMCP 3.2.4 when `ttl=0` was set. Removed since list operations are not cached anyway. |
| 32 | + |
| 33 | +**FastMCP providers**: `Choice` and `GenerativeUI` from `fastmcp.apps` are added to the server in `__init__`. `fastmcp[apps]>=3.2.4` is in `pyproject.toml`. |
| 34 | + |
| 35 | +**Skills directory**: `src/mcp_github/skills/` contains markdown skill files exposed as MCP resources via `SkillsDirectoryProvider` under the `skill://` URI scheme. Each subdirectory (e.g. `pr-analysis/`, `issue-management/`) contains a `SKILL.md` with workflow guidance for the LLM. |
| 36 | + |
| 37 | +**Exposed MCP Tools** (from `GitHubIntegration`): |
| 38 | + |
| 39 | +- `get_pr_diff` — raw patch/diff from patch-diff.githubusercontent.com |
| 40 | +- `get_pr_content` — PR metadata (title, description, author, state) |
| 41 | +- `update_pr_description` — PATCH PR body |
| 42 | +- `create_pr` — open a new pull request |
| 43 | +- `merge_pr` — merge a PR (merge/squash/rebase) |
| 44 | +- `add_pr_comments` — add a general comment to a PR |
| 45 | +- `add_inline_pr_comment` — add an inline review comment at a specific file/line |
| 46 | +- `update_reviews` — submit a PR review (APPROVE/REQUEST_CHANGES/COMMENT) |
| 47 | +- `update_assignees` — assign users to a PR or issue |
| 48 | +- `create_issue` — create a GitHub issue (auto-adds `mcp` label) |
| 49 | +- `update_issue` — update issue title/body/state/labels |
| 50 | +- `list_open_issues_prs` — list open issues or PRs with filtering |
| 51 | +- `get_latest_sha` — get HEAD SHA of default branch |
| 52 | +- `create_tag` — create an annotated git tag |
| 53 | +- `create_release` — publish a GitHub release |
| 54 | +- `search_user` — user profile via GraphQL |
| 55 | +- `get_user_activities` — commit/PR/issue/review contributions via GraphQL |
| 56 | + |
| 57 | +From `IPIntegration`: `get_ipv4_info`, `get_ipv6_info` |
| 58 | + |
| 59 | +**IPv6 socket override**: `IPIntegration.get_ipv6_info()` uses `httpx.HTTPTransport(local_address="::")` to force IPv6 socket family. Do not refactor this into a persistent setting. |
| 60 | + |
| 61 | +**GraphQL Schema Notes** (from recent fixes): |
| 62 | + |
| 63 | +- `CreatedCommitContribution`: has `commitCount`, `url`, `occurredAt` — NOT `commit { message }` |
| 64 | +- `CreatedPullRequestReviewContribution`: has `pullRequestReview { state url }` — NOT `review` |
| 65 | + |
| 66 | +## Development Commands |
| 67 | + |
| 68 | +```bash |
| 69 | +# Setup (Python >=3.14 required) |
| 70 | +uv sync --group dev |
| 71 | + |
| 72 | +# Run (stdio mode) |
| 73 | +export GITHUB_TOKEN="<token>" |
| 74 | +uvx ./ |
| 75 | + |
| 76 | +# Run (HTTP mode — streamable-http transport) |
| 77 | +export GITHUB_TOKEN="<token>" |
| 78 | +export MCP_ENABLE_REMOTE=true |
| 79 | +uvx ./ |
| 80 | + |
| 81 | +# Docker build and run (HTTP mode) |
| 82 | +docker build -t mcp-github . |
| 83 | +docker run -e GITHUB_TOKEN="<token>" -p 8081:8081 mcp-github |
| 84 | + |
| 85 | +# Code quality |
| 86 | +ruff check . --fix |
| 87 | +ruff format . |
| 88 | +uv run pyright src/mcp_github/ |
| 89 | + |
| 90 | +# Tests (pytest is configured in pyproject.toml; no tests exist yet) |
| 91 | +uv run pytest |
| 92 | + |
| 93 | +# Regenerate requirements.txt after dependency changes (auto-generated — do not edit manually) |
| 94 | +uv export --frozen --no-emit-project > requirements.txt |
| 95 | + |
| 96 | +# Test HTTP auth (after starting server in HTTP mode) |
| 97 | +curl -H "Authorization: Bearer <GITHUB_TOKEN>" http://localhost:8081/mcp |
| 98 | +``` |
| 99 | + |
| 100 | +## Environment Variables |
| 101 | + |
| 102 | +| Variable | Required | Default | Description | |
| 103 | +|----------|----------|---------|-------------| |
| 104 | +| `GITHUB_TOKEN` | Yes | - | GitHub PAT with `repo` scope; also used as the bearer token in HTTP mode | |
| 105 | +| `MCP_ENABLE_REMOTE` | No | unset | Any non-empty string enables HTTP/streamable-http mode | |
| 106 | +| `PORT` | No | 8081 | HTTP server port | |
| 107 | +| `HOST` | No | `localhost` | HTTP server host | |
| 108 | +| `GITHUB_API_TIMEOUT` | No | `5` | Timeout in seconds for both REST and GraphQL requests | |
| 109 | +| `GITHUB_OAUTH_CLIENT_ID` | No | unset | GitHub OAuth App client ID — enables OAuth2 auth path via `GitHubProvider` | |
| 110 | +| `GITHUB_OAUTH_CLIENT_SECRET` | No | unset | GitHub OAuth App client secret — required alongside `GITHUB_OAUTH_CLIENT_ID` | |
| 111 | +| `GITHUB_OAUTH_BASE_URL` | No | unset | Public base URL of the server — required by the OAuth2 redirect flow | |
| 112 | +| `JWT_SIGNING_KEY` | No | unset | Secret for signing FastMCP JWT tokens. When set, used as an explicit override. When omitted, a stable key is derived automatically from `GITHUB_OAUTH_CLIENT_SECRET` so all pods share the same key without requiring an additional env var. Rotating the GitHub OAuth App secret invalidates all stored sessions | |
| 113 | +| `FASTMCP_HOME` | No | platformdirs user data dir | Directory for FastMCP state (OAuth client registrations, token store). Set to `/tmp` in the Dockerfile so the read-only K8s filesystem is not a problem. State stored here is ephemeral when backed by an emptyDir; clients re-register automatically after pod restarts. | |
| 114 | +| `REDIS_HOST_PORT` | No | unset | Redis connection string. Accepts `host:port` or a full URI: `redis://[:password@]host:port[/db]` (plaintext) or `rediss://[:password@]host:port[/db]` (TLS). The scheme controls TLS — no extra env var needed. | |
| 115 | +| `REDIS_PASSWORD` | No | unset | Redis AUTH password fallback — used when not embedded in the URI. Store in a K8s Secret, not a ConfigMap. | |
| 116 | + |
| 117 | +**HTTP auth**: In HTTP mode, `APIKeyVerifier` is wired with `GITHUB_TOKEN`. Clients must send `Authorization: Bearer <GITHUB_TOKEN>` — there is no separate API key. |
| 118 | + |
| 119 | +**OAuth2 auth**: If all three `GITHUB_OAUTH_*` vars are set, `_select_auth()` routes to `GitHubProvider` (FastMCP's OAuth proxy) instead of `APIKeyVerifier`. `GitHubProvider` implements the full OAuth 2.1 + Dynamic Client Registration flow: MCP clients register at `/register`, get a UUID `client_id`, then authorize via the consent/GitHub OAuth flow. Token state is stored via `build_token_store()` — in-process `MemoryStore` by default (no disk writes, sessions lost on restart), or a namespaced `RedisStore` when `REDIS_HOST_PORT` is set. When Redis is used, all collection names are automatically prefixed with a 12-char SHA-256 hash of `GITHUB_OAUTH_BASE_URL`, so two server instances sharing the same Redis have fully isolated keyspaces. **If the server restarts in memory-only mode, cached client_ids become stale — instruct users to clear MCP client auth tokens and reconnect.** If two servers use the same `GITHUB_OAUTH_CLIENT_ID` and the same GitHub account, MCP clients may still conflate their OAuth sessions at the client level (both JWTs embed the same `client_id`); use a separate GitHub OAuth App per logical server to avoid this. |
| 120 | + |
| 121 | +**`TokenVerifier` subclass requirement**: Any subclass of `TokenVerifier` (or `AuthProvider`) **must call `super().__init__()`**. The parent sets `base_url`, `required_scopes`, `_mcp_path`, and `_resource_url` — FastMCP accesses these when building the HTTP app and will raise `AttributeError` if they are absent. |
| 122 | + |
| 123 | +## Key Files |
| 124 | + |
| 125 | +- **Entry point**: `src/mcp_github/issues_pr_analyser.py:main()` — also exposed as `mcp-github-pr-issue-analyser` script |
| 126 | +- **Auth providers**: `src/mcp_github/auth.py` — `APIKeyVerifier`, `get_oauth_verifier()`, and token resolution |
| 127 | +- **GraphQL queries**: `src/mcp_github/graphql_queries.py` — verify fields against GitHub schema before modifying |
| 128 | +- **GraphQL client**: `src/mcp_github/graphql_client.py` — thin wrapper used by `github_integration.py` for all v4 API calls |
| 129 | +- **Exception hierarchy**: `src/mcp_github/exceptions.py` — `MCPGitHubError` -> `GitHubAPIError` -> `GitHubAuthError` / `GitHubRateLimitError` / `GitHubNotFoundError` / `GitHubValidationError`; also `IPInfoError` |
| 130 | +- **Return types**: `github_integration.py` defines TypedDicts as PEP 695 type aliases (`type PRContent = TypedDict(...)`) used as return type annotations |
| 131 | +- **Skills**: `src/mcp_github/skills/` — MCP resources exposed under `skill://` URIs; loaded via `SkillsDirectoryProvider` at startup |
| 132 | +- **Registry manifest**: `registry/server.yaml` — metadata for publishing to MCP server registries |
| 133 | +- **Version**: managed by `setuptools-scm` from git tags; `src/mcp_github/_version.py` is auto-generated — do not edit |
| 134 | + |
| 135 | +## Code Style |
| 136 | + |
| 137 | +**No lazy imports**: All imports must be at the top of the file. Never use `import` statements inside functions or conditional blocks. Maintain consistency across all modules in the repo. |
| 138 | + |
| 139 | +**Docstrings (NumPy style)**: |
| 140 | +- All docstrings: one blank line after the opening `"""`, summary on the second line (D213). For class docstrings, add one blank line before the `"""` as well (D203). |
| 141 | +- Section headers use no trailing colon and a dashed underline on the next line, e.g.: |
| 142 | + ``` |
| 143 | + Raises |
| 144 | + ------ |
| 145 | + RuntimeError |
| 146 | + Description here. |
| 147 | +
|
| 148 | + ``` |
| 149 | + Always leave one blank line after the last section before the closing `"""` (D406, D407, D413). |
| 150 | +- Ruff ignores both D212/D213 and D203/D211 (they are mutually exclusive pairs). Codacy enforces D213 + D203 — use that convention. |
| 151 | + |
| 152 | +**Markdown headings**: Leave one blank line between a heading and the content that follows it. |
| 153 | + |
| 154 | +## Task Backlog |
| 155 | + |
| 156 | +`tasks/` contains numbered markdown files for planned bug fixes, improvements, and features. Check here before starting new work to avoid duplicating planned effort. |
| 157 | + |
| 158 | +## Testing |
| 159 | + |
| 160 | +Tests live in `tests/` with `test_*.py` naming (configured in `pyproject.toml`). Run with `uv run pytest`. Bandit security scanning excludes the `tests/` directory (configured in `pyproject.toml` under `[tool.bandit]`) — `assert` statements and test-fixture strings are expected and not flagged. |
| 161 | + |
| 162 | +## CI/CD |
| 163 | + |
| 164 | +GitHub Actions in `.github/workflows/`: |
| 165 | + |
| 166 | +- `ci.yml` — CodeQL, dependency review, Docker build/push to `ghcr.io/saidsef/mcp-github-pr-issue-analyser` (multi-platform: amd64, arm64), Trivy scan |
| 167 | +- `tag_release.yml` — Automated semantic versioning and releases on push to main |
| 168 | + |
| 169 | +CI auto-approves PRs after successful build. Do not manually update version numbers in PRs. |
| 170 | + |
| 171 | +`uv.lock` should be committed — it is the authoritative lockfile for `uv sync` and ensures reproducible installs. |
| 172 | + |
| 173 | +## Deployment |
| 174 | + |
| 175 | +Kubernetes manifests in `deployment/` use Kustomize (`kustomization.yml`). The Dockerfile sets `MCP_ENABLE_REMOTE=true` and `PORT=8081` as defaults — the container always runs in HTTP mode. |
| 176 | + |
| 177 | +**K8s read-only filesystem**: The pod spec sets `readOnlyRootFilesystem: true`. `requirements.txt` contains `-e .` (editable install, auto-generated by `uv export`) which would cause `setuptools-scm` to write `_version.py` and `.egg-info/` at runtime. The Dockerfile handles this by stripping `-e .` before installing and then doing a non-editable install: |
| 178 | +```dockerfile |
| 179 | +grep -v "^-e " requirements.txt > /tmp/requirements.txt && \ |
| 180 | +uv pip install --system -r /tmp/requirements.txt && \ |
| 181 | +uv pip install --system --no-deps . |
| 182 | +``` |
| 183 | +Do not change `CMD` back to `uv run` — it re-triggers the editable build at pod startup. |
| 184 | + |
| 185 | +To simulate K8s locally: |
| 186 | +```sh |
| 187 | +docker build -t mcp-test --build-arg SETUPTOOLS_SCM_PRETEND_VERSION=0.0.0 . |
| 188 | +docker run --read-only --tmpfs /tmp -e GITHUB_TOKEN="<token>" -p 8081:8081 mcp-test |
| 189 | +``` |
0 commit comments