Skip to main content
Glama
sam170203

Personal GitHub MCP

by sam170203
README.md
# Personal GitHub MCP

A production-grade [Model Context Protocol](https://modelcontextprotocol.io) server that acts as a **personal GitHub engineering assistant**.

Instead of exposing raw GitHub API calls, it provides high-level tools that represent real user intentions — publish the project you are working in, upload a Project Euler solution with an auto-generated README, find where you used a library, or search your entire engineering memory.

Built with **Python 3.12+**, the **official MCP Python SDK (v2)**, **PyGithub**, **GitPython**, **Pydantic v2**, and **uv**.

---

## Features

- **Repository management** — list, summarize, create, and inspect your repositories.
- **Publishing** — `publish_current_project` publishes the directory you are in with an auto-generated README and `.gitignore`; `publish_project` / `backup_project` cover the rest.
- **Project Euler flagship workflow** — `publish_euler_solution` detects the problem from the current directory, infers the language, generates a professional README (LLM or static analysis), and pushes both to your solutions repo.
- **Search** — GitHub code/repo search plus `search_my_engineering_memory`, which ranks hits across your repositories, READMEs, architecture docs, and source code.
- **Analysis** — explain a repo in plain language, compare repos, recommend what to work on, detect stale repos, and summarize a local project's dependencies and architecture.
- **Structured, typed errors** — the server never crashes; every failure is returned as a structured envelope.
- **Structured JSON logging** with per-tool timing.

Every tool returns the same envelope:

```json
{ "ok": true, "data": { ... } }
```

or

```json
{ "ok": false, "error": { "kind": "repository_not_found", "message": "..." } }
```

---

## Architecture

```
src/
├── server.py              # MCP server assembly + tool registration wiring
├── main.py                # CLI entry point (uv run personal-github-mcp)
├── config.py              # Settings loaded from .env (pydantic-settings)
├── models.py              # Pydantic response models for every tool
├── github_client.py       # Thin PyGithub wrapper (exception mapping)
├── git_client.py          # Thin GitPython wrapper (exception mapping)
├── dependencies.py        # Service container / dependency wiring
├── tools/                 # Tool registration only (thin, one concern per file)
│   ├── repo.py            #   repository listing/summary/create + reporting
│   ├── publish.py         #   publish_project, publish_current_project, backup_project
│   ├── search.py          #   search_code, search_repository, where_did_i_use,
│   │                      #   search_my_engineering_memory
│   ├── euler.py           #   upload_euler_solution, publish_euler_solution, update_progress
│   └── analyze.py         #   compare_repositories, dependency_summary, ...
├── services/              # Business logic (SOLID, no raw API calls here)
│   ├── github_service.py  #   core GitHub CRUD (list/summary/create/exists)
│   ├── report_service.py  #   statistics, explain, stale report, recommend, compare
│   ├── git_service.py     #   local git + project analysis
│   ├── search_service.py  #   code/repo search + engineering memory search
│   ├── euler_service.py   #   Project Euler detection, upload, publish, progress
│   ├── project_service.py #   publish / backup workflows
│   ├── readme_service.py  #   README generation (static analysis + optional LLM)
│   ├── scaffold_service.py#   README + .gitignore scaffolding for projects
│   └── llm_service.py     #   optional OpenAI-compatible client (never required)
└── utils/
    ├── logging.py         # Structured JSON logging
    └── errors.py          # Typed errors + tool_handler decorator
```

Layering is strict:

```
tools (MCP registration only)
   → services (business logic, returns Pydantic models)
      → clients (PyGithub / GitPython wrappers, raise typed errors)
         → utils (shared errors + logging)
```

This keeps tool files small, makes the business logic unit-testable, and keeps
the server module free of implementation details. Services are
dependency-injected and model-backed so they can be lifted straight into
LangGraph agents later.

---

## Installation

Prerequisites: Python 3.12+, [git](https://git-scm.com), and [uv](https://docs.astral.sh/uv/).

```bash
git clone <your-repo-url> personal-github-mcp
cd personal-github-mcp

# Install dependencies and the package (editable dev install)
uv sync --all-extras

# Configure secrets
cp .env.example .env
# edit .env and set GITHUB_TOKEN
```

---

## Configuration

Copy `.env.example` to `.env` and fill in at least `GITHUB_TOKEN`.

| Variable | Required | Default | Description |
| --- | --- | --- | --- |
| `GITHUB_TOKEN` | yes | — | GitHub Personal Access Token (classic `repo` scope, or a fine-grained token with read/write on your repos). `PERSONAL_GITHUB_TOKEN` is accepted as an alias. |
| `GITHUB_USERNAME` | no | token owner | Account used to scope searches/repo listing. |
| `EULER_REPOSITORY` | no | `project-euler` | Repo that stores Project Euler solutions. |
| `DEFAULT_VISIBILITY` | no | `private` | Visibility for new repos (`private`/`public`). |
| `DEFAULT_BRANCH` | no | `main` | Branch used when initializing new repos. |
| `GIT_AUTHOR_NAME` | no | `Personal GitHub MCP` | Author for local git commits. |
| `GIT_AUTHOR_EMAIL` | no | `personal-github-mcp@users.noreply.github.com` | Committer email for local commits. |
| `LOG_LEVEL` | no | `INFO` | `DEBUG`, `INFO`, `WARNING`, `ERROR`. |
| `MAX_RESULTS` | no | `20` | Default result cap for list/search tools (1–100). |
| `OPENAI_API_KEY` | no | — | Optional OpenAI-compatible API key for LLM-generated READMEs (`LLM_API_KEY` also accepted). |
| `OPENAI_BASE_URL` | no | `https://api.openai.com/v1` | Base URL for an OpenAI-compatible `chat/completions` endpoint (`LLM_BASE_URL` also accepted). |
| `OPENAI_MODEL` | no | `gpt-4o-mini` | Model used for README generation (`LLM_MODEL` also accepted). |

> **Never commit `.env`.** It is git-ignored. If the token is missing at startup
> the server prints a clear message and exits with a non-zero code.
>
> **README generation never fails without an LLM.** When no `OPENAI_API_KEY`
> is set (or the call fails), READMEs are generated deterministically from
> static analysis of the project/solution.

---

## Tools

### Repository

| Tool | Description |
| --- | --- |
| `list_repositories(visibility, sort, limit)` | List your repos, optionally filtered by `private`/`public` and sorted by `pushed`/`updated`/`created`/`full_name`. |
| `repository_summary(name)` | High-level summary: languages, topics, last commit, top-level structure. |
| `create_repository(name, description, private)` | Create a repo under your account (auto-initialized with a README). |
| `project_statistics(name)` | Language breakdown, commits per author, open issues/PRs. |
| `explain_repository(name)` | Plain-language explanation of what a repo is about. |
| `stale_repository_report(threshold_days, limit)` | Repos not pushed to for a while. |
| `recommend_project(interests, limit)` | Ranked suggestions for what to work on next. |

### Publishing

| Tool | Description |
| --- | --- |
| `publish_current_project(repo_name, description, private, commit_message, branch)` | Publish the **current working directory**. Detects the project, generates a README and `.gitignore` when missing, initializes git, creates the GitHub repo if needed, commits and pushes. Returns the repo URL. |
| `publish_project(path, repo_name, description, private, commit_message, branch)` | Publish a local directory as a **new** GitHub repo (init → commit → create repo → push). |
| `backup_project(path, commit_message)` | Commit local changes and push to an existing repo (creates the repo only if no remote exists). |

Publishing handles every combination: existing repos, not-yet-created repos,
repos with a remote, and repos without one.

### Search

| Tool | Description |
| --- | --- |
| `search_code(keyword, language, owner, limit)` | GitHub code search. |
| `search_repository(name, owner, limit)` | GitHub repository search by name. |
| `where_did_i_use(keyword, language, limit)` | Find where *you* used a keyword in your own repos. |
| `search_my_engineering_memory(keyword, limit)` | Search across your repositories, README files, architecture documentation, and source code, returning ranked results. |

### Project Euler

| Tool | Description |
| --- | --- |
| `publish_euler_solution(path, commit_message)` | **Flagship workflow.** Detects the problem in the current directory (folders like `001/` or `problem_023/`), infers the language and source file, generates a professional README, and uploads the solution + README to your Euler repo. |
| `upload_euler_solution(problem_number, file_path, commit_message)` | Upload a solution as `problem_<NNN>/<filename>` in your Euler repo. |
| `update_progress()` | Solved problems, totals, and the next unsolved problem number. |

Generated Euler READMEs include: **Problem Number**, **Problem Statement**
(placeholder — the official statement is copyright and cannot be reproduced),
**Approach**, **Complexity** (time & space), **Key Insights**, and **Files**.

### Analysis

| Tool | Description |
| --- | --- |
| `compare_repositories(repository_a, repository_b)` | Compare two repos and summarize differences. |
| `dependency_summary(path)` | Detect and list a local project's dependencies. |
| `architecture_summary(path)` | Languages, structure, and entry points of a local project. |

---

## Example usage

```jsonc
// publish the project you're currently in (detects cwd, writes README + .gitignore)
publish_current_project(description: "my notes app")
// → { "ok": true, "data": { "repository": "me/notes-app", "url": "https://github.com/me/notes-app",
//     "created": true, "readme_generated": true, "gitignore_generated": true, ... } }

// flagship Project Euler workflow (run from inside "ProjectEuler/023/")
publish_euler_solution()
// → { "ok": true, "data": { "problem_number": 23, "path_in_repo": "problem_023/solution.py",
//     "readme_url": "https://github.com/me/project-euler/...", ... } }

// search your entire engineering memory
search_my_engineering_memory(keyword: "sieve")
// → { "ok": true, "data": { "query": "\"sieve\" user:me", "items": [
//     { "kind": "repository", "repository": "me/algo", ... },
//     { "kind": "source", "repository": "me/algo", "path": "sieve.py", ... } ] } }

// publish a local project by path
publish_project(path: "/Users/me/projects/notes", repo_name: "notes", private: true)
// → { "ok": true, "data": { "repository": "me/notes", "url": "https://github.com/me/notes", "created": true, ... } }

// upload a Project Euler solution
upload_euler_solution(problem_number: 25, file_path: "/Users/me/euler/p025.py")
// → { "ok": true, "data": { "problem_number": 25, "path_in_repo": "problem_025/p025.py", ... } }
```

---

## Connecting from Claude Desktop

Add this to your Claude Desktop config (`claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "personal-github-mcp": {
      "command": "uv",
      "args": ["--directory", "/absolute/path/to/personal-github-mcp", "run", "personal-github-mcp"],
      "env": {
        "GITHUB_TOKEN": "ghp_xxxx"
      }
    }
  }
}
```

If your token is already in the project's `.env`, you can omit `env` — the server loads `.env` itself.

---

## Connecting from Cursor

In Cursor, add an MCP server (Settings → MCP) with the stdio type:

```json
{
  "mcpServers": {
    "personal-github-mcp": {
      "type": "stdio",
      "command": "uv",
      "args": ["--directory", "/absolute/path/to/personal-github-mcp", "run", "personal-github-mcp"],
      "env": {
        "GITHUB_TOKEN": "ghp_xxxx"
      }
    }
  }
}
```

The same pattern works for other MCP clients (Claude Code, VS Code Copilot, etc.): point the client at `uv --directory <path> run personal-github-mcp`.

---

## Screenshots

| Area | Placeholder |
| --- | --- |
| Claude Desktop tools | `docs/screenshots/claude-tools.png` |
| `publish_euler_solution` result | `docs/screenshots/euler-publish.png` |
| Engineering memory search | `docs/screenshots/memory-search.png` |

---

## Troubleshooting

**"GitHub token is required" and the server exits with code 2**
Set `GITHUB_TOKEN` in `.env` (or export it) and retry. The server deliberately
refuses to start without a token.

**`publish_euler_solution` says "No Project Euler problem detected"**
The tool expects to run inside (or above) a folder named like `001`,
`problem_023`, or `ProjectEuler/023`, and that folder must contain a recognized
source file (`solution.py`, `main.cpp`, etc.). Supported extensions: `.py`,
`.cpp`, `.cc`, `.cxx`, `.c`, `.rs`, `.go`, `.js`, `.ts`, `.java`, `.rb`, `.cs`,
`.php`, `.swift`, `.kt`, `.hs`, `.lua`, `.zig`, `.ml`.

**"Git push failed"**
A remote may point to a repository you cannot write to, or the branch has
diverged. Run `git remote -v` and `git log` to diagnose; `backup_project` is
the safe way to push to an existing remote.

**GitHub API / rate-limit errors**
GitHub rate limits apply. Wait and retry, or reduce `limit`/`MAX_RESULTS`.
Fine-grained tokens need read/write access to the repos you manage.

**READMEs look generic**
No LLM is configured, so READMEs come from static analysis. Set
`OPENAI_API_KEY` (and optionally `OPENAI_BASE_URL`/`OPENAI_MODEL`) to get
LLM-written READMEs. Any LLM failure falls back to static analysis silently.

**Logs are JSON lines**
Set `LOG_LEVEL=DEBUG` for more detail. Per-tool timing and error kinds are
logged to stderr.

---

## Testing

```bash
uv run pytest
```

The suite covers GitHub services (with in-memory fakes), reporting/analysis
services, git services (real repos in temp dirs, including pushes to a local
bare remote), the full publish/backup workflows, Project Euler detection and
publishing, README/.gitignore scaffolding, search, MCP tool registration and
response envelopes.

## Development

```bash
uv sync --all-extras        # install everything
uv run ruff format src tests
uv run ruff check src tests
uv run pytest
```

---

## Roadmap

- **LangGraph integration** — services are deliberately dependency-injected
  and model-backed so they can be lifted straight into LangGraph agents/nodes.
- **Project Euler templates** — generate solution scaffolds per problem.
- **PR automation** — raise pull requests from local branches.
- **SSE / HTTP transport** — support `transport="streamable-http"` for remote clients.
- **Issues triage** — list and label open issues across repositories.
- **Auth UX** — OAuth device flow as an alternative to a PAT.
- **Fuzzy "where did I use"** — tokenized search across clone history, not just the API.

TDQS

B3.2/5.0

Scored across 20 tools

Disambiguation2/5

Several tools have overlapping purposes: repository_summary, explain_repository, and project_statistics all describe repository characteristics; publish_project and publish_current_project both publish local projects; and search_code, where_did_i_use, and search_my_engineering_memory all perform search operations. These unclear boundaries make it difficult for an agent to reliably select the correct tool.

Naming Consistency2/5

Naming is mixed between verb-first (list_repositories, create_repository, search_code) and noun-first (repository_summary, project_statistics, dependency_summary) patterns, with the exception of 'where_did_i_use' which uses a completely different sentence-like format. The inconsistency is significant enough to hinder predictable tool selection.

Tool Count3/5

With 20 tools, the server is on the heavy side—slightly above the ideal 3–15 range. While the count is not outrageous, the many overlapping tools inflate the number and make the set feel heavier than it needs to be.

Completeness2/5

The server covers repository listing, creation, searching, publishing, and local project analysis, but lacks basic GitHub operations such as updating or deleting repositories, managing issues and pull requests, or viewing commit history. These are significant gaps for a GitHub-related MCP, leaving core workflows incomplete.

Maintenance

ActivitySlowing
ResponsivenessNo issues