# <img src="https://gitlab.com/b.rajaut/project-hub/-/raw/main/project_hub/web/static/logo.svg" width="32" height="32" align="absmiddle"> project-hub

Multi-repo workspace manager that unifies local git state and forge data (MRs, issues, pipelines, vulnerabilities) into a single **MCP server + web dashboard**.

Designed for engineers managing many repos under one parent directory — across multiple forges.

![Dashboard](https://gitlab.com/b.rajaut/project-hub/-/raw/main/.gitlab/dash.jpg)

## Killer features

*aka why I built this*

**GitLab, GitHub, Bitbucket in a single interface.** No more context-switching between forges. project-hub auto-detects which forge hosts your repos and talks to all of them — MRs, issues, pipelines, everything in one dashboard. Add a repo on Bitbucket, another on GitLab, a third on GitHub: same view, same tools, same agent.

**Vulnerabilities that make sense.** GitLab's vulnerability report is per-repo, noisy, and doesn't tell you what's already being fixed. project-hub aggregates vulns across all your repos, cross-references them with open MR branches to flag what's already fixed but not yet merged ("fixed in branch"), and lets you filter by severity, state, or hide already-handled ones. You see what actually needs work, not what's already in flight.

**Stale branch detection.** After merging an MR you often forget to switch back to the default branch locally. project-hub detects repos still sitting on stale feature branches and shows ahead/behind relative to the default branch — with a one-click checkout to clean up.

**Events, not inbox.** Stop reminding yourself to check your cluttered GitLab inbox. project-hub diffs state on every refresh cycle and surfaces what changed: new MR, issue opened/closed, pipeline failure, new vuln, auth expiry. Events live in a slide-in panel on the dashboard and persist across sessions until dismissed.

**MCP-powered multi-repo agent.** Everything visible in the dashboard is exposed as MCP tools. Claude Code sees your repos, MRs, issues, pipelines, vulns, and events — and can act on them. Events surface as MCP notifications, making the agent proactive instead of reactive.

## Features

- **Unified dashboard** — git status, MRs, issues, pipelines, vulns across all repos in one view
- **Merge request, issue & pipeline views** — cross-repo tables with filters (mine/all, failed only, current branches only)
- **Per-repo detail page** — MRs, issues, pipelines, active vulns for a single repo
- **Web UI** (htmx) with real-time sync, sortable tables, persistent filters
- **Non-destructive pull** — conflict detection via `git merge-tree`, no worktree mutations
- **Pin & reorder** repos, drag-and-drop card layout
- **Bulk actions** — pull all, unstale all in one click

## Quick start

```bash
cd ~/Projects                    # parent dir containing your repos
uvx project-hub init             # creates .project-hub/
uvx project-hub auth login gitlab.com     # store your GitLab PAT
uvx project-hub auth login github.com     # store your GitHub token
uvx project-hub auth login bitbucket.org  # store your Bitbucket token
uvx project-hub                  # launch dashboard (opens browser)
```

### Use as MCP server (Claude Code)

From PyPI:

```bash
claude mcp add --scope user project-hub -- uvx project-hub --mcp
```

From source (picks up local changes without cache issues):

```bash
claude mcp add --scope user project-hub -- \
  uv run --project /path/to/project-hub project-hub --mcp
```

Installing with `--scope user` is safe — the server adapts to the current directory:

| Directory state | Behavior |
|----------------|----------|
| Has `.project-hub/` | Active — full tools + refresh |
| Parent has `.project-hub/` | Disabled — zero tools (sub-repo) |
| Has `.no-project-hub` | Disabled — zero tools (opt-out) |
| Neither | Dormant — only `init` tool |

```bash
touch ~/some-project/.no-project-hub   # force disable in a specific dir
```

Then in Claude Code, the agent can call tools like `list_repos`, `get_vulnerabilities`, `pull_safe`, etc.

## MCP tools

| Tool | Description |
|------|-------------|
| `init` | Bootstrap `.project-hub/` workspace |
| `list_repos` | List discovered repos with forge info |
| `repo_status` | Git status (branch, ahead/behind, dirty) |
| `fetch_all` | Trigger full refresh cycle |
| `pull_safe` | Pull with conflict detection |
| `list_mrs` | Merge requests (substring repo filter) |
| `list_issues` | Issues (repo, state, author, label filters) |
| `list_pipelines` | Pipelines (substring repo filter) |
| `get_vulnerabilities` | Vulns — defaults to high/critical + detected, includes solutions |
| `get_pipeline_jobs` | Jobs for a specific pipeline |
| `acknowledge_alerts` | Mark events as seen |
| `auth_status` | Check forge authentication |
| `auth_login` | Store PAT for a forge instance |
| `configure_forge` | Configure forge host mapping |

## Web dashboard

| Route | Description |
|-------|-------------|
| `/` | Dashboard with repo cards, pinning, drag-and-drop, events panel |
| `/mrs` | Merge requests table (mine/all filter) |
| `/issues` | Issues table (mine/all filter) |
| `/pipelines` | Pipelines table (failed only, current branches only) |
| `/vulns` | Vulnerabilities (severity, state, fixed-in-branch filters) |
| `/repo/{name}` | Per-repo detail: MRs, issues, pipelines, active vulns |
| `/settings` | Exclude/include repos, rescan workspace |

All filters and table sort are persisted in localStorage.

## Architecture

Single Python process, dual interfaces: MCP (stdio) + HTTP (FastAPI/htmx). Background asyncio task refreshes git+forge data on a TTL cycle, diffs with SQLite cache to generate events.

```
project_hub/
  cli.py              # Click entry points
  core/
    workspace.py       # Orchestrator: discover -> refresh loop -> stop
    cache.py           # SQLite (WAL mode, aiosqlite)
    config.py          # YAML config with defaults
    discovery.py       # Scan for .git/ repos, classify forges
    models.py          # Dataclasses: Repo, MR, Issue, Pipeline, Vulnerability, Event
    events.py          # Diff old vs new state -> Event list
  forge/
    provider.py        # ForgeProvider ABC (shared interface for all forges)
    auth.py            # Token resolution: auth.json -> env vars -> config files
    gitlab.py          # python-gitlab + GraphQL
    github.py          # PyGithub
    bitbucket.py       # atlassian-python-api (Cloud)
  git/
    ops.py             # fetch, status, pull_safe (merge-tree conflict detection)
  mcp/
    server.py          # FastMCP tools + resources
  web/
    app.py             # FastAPI factory, routes, htmx fragments
    templates/         # Jinja2 templates
    static/            # CSS + vendored JS
```

## Requirements

- Python 3.10+
- Git 2.38+ (for `git merge-tree --write-tree`)
- A token for each forge you use:
  - GitLab: PAT with `read_api` scope
  - GitHub: PAT with `repo` scope
  - Bitbucket Cloud: API token (app passwords deprecated June 2026)

## Testing

```bash
uv run pytest tests/ -v
```

## License

[Unlicense](https://unlicense.org/) — public domain.
