slim-github-mcp
by 0xBakeer
README.md
# slim-github-mcp
A read-only GitHub MCP server that returns **small** results.
Most GitHub MCP servers hand the model raw GitHub API objects. That is fine for a
frontier model behind a fast API and painful for a local one: a single page of
repository search results is around **14 KB of JSON (~3.5k tokens)**, most of it URLs
and permission flags nothing will ever read. Add ~7k tokens of tool schemas on top and
a locally-hosted model can spend minutes on "list my repositories" — or never finish.
This server takes the opposite position. Every tool returns a handful of useful fields,
every list is capped server-side, and the whole tool surface is nine functions. The
model cannot pull a huge payload, no matter what arguments it invents.
| | typical GitHub MCP | slim-github-mcp |
|---|---|---|
| tool schemas | ~27 tools, ~7.4k tokens | 9 tools, ~830 tokens |
| "my public repos" | not a tool — needs a search | `gh_my_repos(visibility="public")` |
| one repo listing | ~14 KB (30 full objects) | ~2.4 KB (10 trimmed objects) |
| writes | usually available | impossible by construction |
## Tools
| Tool | Returns |
|---|---|
| `gh_whoami()` | The authenticated account: login, name, public repo count |
| `gh_my_repos(visibility, limit, page)` | Your own repos, newest-updated first. `visibility`: `all` \| `public` \| `private` |
| `gh_user_repos(username, limit, page)` | Public repos of any user or org |
| `gh_search_repos(query, limit, sort)` | Repository search. `sort`: `stars` \| `forks` \| `updated` |
| `gh_search_code(query, limit)` | Code search — repo, path and URL per hit, no snippets |
| `gh_issues(repo, state, limit)` | Issues of `owner/name` |
| `gh_pulls(repo, state, limit)` | Pull requests of `owner/name` |
| `gh_commits(repo, limit, branch)` | Recent commits — sha, date, author, subject line |
| `gh_file(repo, path, ref)` | One file's contents, or a directory listing |
Every `limit` defaults to **10** and is clamped to **30**. `gh_file` truncates at 20k
characters and says so via a `truncated` flag. Repos come back as `full_name`,
`private`, a 160-character `description`, `language`, `stars`, `updated`, `url` —
nothing else.
There is deliberately no write path: the client only ever issues `GET` to
`api.github.com`.
## Getting a GitHub token
The server needs one token, used for every call. Only read access is required.
1. Go to **GitHub → Settings → Developer settings → Personal access tokens →
Fine-grained tokens → Generate new token**.
2. Set an expiry and choose which repositories the token may see — *Public
repositories only* if you never want private data to leave GitHub, or *All
repositories* if you want `gh_my_repos(visibility="private")` to work.
3. Under **Repository permissions**, grant read-only access to what you actually need:
- **Metadata: Read-only** — required, and enough for repo listings and search
- **Contents: Read-only** — for `gh_file` and `gh_commits`
- **Issues: Read-only** — for `gh_issues`
- **Pull requests: Read-only** — for `gh_pulls`
4. Leave every *Account permission* alone.
5. Copy the token — GitHub shows it once.
A classic token with the `repo` scope also works, but it is far broader than this
server needs; prefer a fine-grained one.
> Unauthenticated GitHub API calls are limited to 60 requests/hour, so a token is not
> optional in practice. With one you get 5,000/hour.
## Installation
```bash
git clone https://github.com/0xBakeer/slim-github-mcp.git
cd slim-github-mcp
python3 -m venv venv
./venv/bin/pip install -r requirements.txt
cp .env.example .env
$EDITOR .env # put your token in GITHUB_MCP_TOKEN
```
Run it:
```bash
set -a; . ./.env; set +a
./venv/bin/uvicorn slim_github_mcp.main:app --host 127.0.0.1 --port "${GITHUB_MCP_PORT:-9130}"
```
Check it came up, and that the token works:
```bash
curl -s http://127.0.0.1:9130/api/health
curl -s -H "Authorization: Bearer $GITHUB_MCP_SECRET" \
'http://127.0.0.1:9130/api/my-repos?visibility=public&limit=5'
```
The MCP endpoint itself is:
```
http://127.0.0.1:9130/tools/mcp/v1
```
It speaks streamable HTTP, so any MCP client that accepts a URL can use it.
### Configuration
| Variable | Default | Meaning |
|---|---|---|
| `GITHUB_MCP_TOKEN` | — | GitHub token used for all API calls. Required. |
| `GITHUB_MCP_SECRET` | *(empty)* | Shared secret clients must send as `Authorization: Bearer <secret>`. Empty disables auth. |
| `GITHUB_MCP_PORT` | `9130` | Port to listen on. |
| `GITHUB_MCP_PUBLIC_HOST` | *(empty)* | Public hostname when behind a reverse proxy, e.g. `github-mcp.example.com`. |
**Set `GITHUB_MCP_SECRET` if the port is reachable by anything but localhost.** The
server holds a token that may be able to read your private repositories, and it does
not otherwise authenticate callers. Generate one with `openssl rand -hex 24`.
`GITHUB_MCP_PUBLIC_HOST` exists because MCP enables DNS-rebinding protection by
default: requests arriving through a reverse proxy carry the public `Host` header and
are rejected unless that hostname is declared. Symptom if you forget it is a `400`
from the MCP endpoint while `/api/health` answers fine.
### Running as a service
`scripts/github-mcp.service` is a systemd unit to adapt — set `User`, the paths, and
point `EnvironmentFile` at a root-owned `0600` file holding the variables above. Keep
the token out of the unit file itself.
There is also a `Dockerfile`:
```bash
docker build -t slim-github-mcp .
docker run -d --name github-mcp -p 127.0.0.1:9130:9130 --env-file .env slim-github-mcp
```
### Behind a reverse proxy
Nothing unusual, but MCP streams, so buffering must be off:
```nginx
location /tools/mcp/ {
proxy_pass http://127.0.0.1:9130;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
}
```
Set `GITHUB_MCP_PUBLIC_HOST` to the hostname you proxy from.
## Using it with Open WebUI
Open WebUI speaks MCP natively (0.6.x and later). Register the server with the helper
script:
```bash
export OPEN_WEBUI_URL=https://webui.example.com
export OPEN_WEBUI_KEY=sk-... # Settings -> Account -> API keys
export GITHUB_MCP_URL=http://127.0.0.1:9130/tools/mcp/v1
export GITHUB_MCP_SECRET=... # if you set one
./scripts/register-open-webui.sh
```
It merges the entry into Open WebUI's existing tool servers (matched by id, so
re-running just updates it) and then prints the tools it discovered:
```
==> Verifying tool discovery
9 tools: gh_whoami, gh_my_repos, gh_user_repos, ...
```
To do it by hand instead: **Admin Settings → Tools → Add Tool Server**, type `MCP`,
URL as above, Auth `Bearer` with your secret.
Two things that are easy to miss:
- **Enable the tool in the chat's tool picker.** A registered server is not
automatically offered to the model.
- **If you pin tools per model** (Workspace → Models → Tools), add it there too. Open
WebUI stores that as `server:mcp:<id>`. Rename the server's id later and the pinned
reference silently resolves to nothing — the model keeps answering, just without the
tools, usually by guessing.
Worth knowing: calling `/api/chat/completions` directly with `tool_ids` does **not**
attach tool servers. Tool wiring happens in the frontend, so test through the UI.
## License
MIT
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues