Skip to main content
Glama
Rhytham4306

github-mcp-server

by Rhytham4306
README.md
# github-mcp-server

A custom MCP (Model Context Protocol) server that connects an LLM to
a GitHub repository's issues — with strict safety guardrails: **read
operations run freely, write operations require an explicit two-step
confirmation before anything is sent to GitHub.**

Built as a demonstration of MCP server design: authenticated API
access, a small typed tool surface, and a confirmation pattern that
can't be bypassed by prompt phrasing.

## Why this exists

MCP is the emerging standard for giving LLMs permissioned access to
private/authenticated tools and data — beyond what web search can
reach. This project shows the full loop: a real external API
(GitHub), a real auth token, and a safety layer that a model can't
talk its way around.

## Architecture

```
github-mcp-server/
├── server.py         # MCP server: tool definitions & routing
├── github_client.py  # Thin async GitHub REST API wrapper
├── confirmation.py   # Two-step confirmation token store for writes
├── requirements.txt
├── .env.example
└── README.md
```

**Tools exposed:**

| Tool | Type | Description |
|---|---|---|
| `list_issues` | read | List issues in a repo, filterable by state/labels |
| `get_issue` | read | Full detail on one issue |
| `search_issues` | read | GitHub search syntax across issues |
| `create_issue` | write (confirm) | Open a new issue |
| `comment_on_issue` | write (confirm) | Post a comment |
| `close_issue` | write (confirm) | Close an issue |

**Confirmation pattern:** every write tool, when called without a
`confirm_token`, returns a preview of the exact action it *would*
take and a short-lived token — it does not touch GitHub yet. The
caller (model or human) reviews the preview and calls the tool again
with that token to actually execute. Tokens are single-use, expire
after 5 minutes, and are bound to the exact arguments — changing the
arguments invalidates the token. This means a write can never happen
on the first call, regardless of how the request is phrased.

## Setup (VS Code)

1. **Clone / open this folder in VS Code.**

2. **Create a virtual environment and install dependencies:**
   ```bash
   python3 -m venv .venv
   source .venv/bin/activate      # Windows: .venv\Scripts\activate
   pip install -r requirements.txt
   ```

3. **Create a GitHub token:**
   GitHub → Settings → Developer settings → Personal access tokens →
   Fine-grained tokens. Scope it to the specific repo(s) you want,
   with **Issues: Read and write** permission only (don't grant more
   than the server needs).

4. **Configure environment:**
   ```bash
   cp .env.example .env
   # then edit .env and fill in GITHUB_TOKEN and GITHUB_DEFAULT_REPO
   ```

5. **Run it standalone to sanity-check it boots:**
   ```bash
   python server.py
   ```
   It will sit waiting on stdio — that's correct, it's meant to be
   driven by an MCP client, not run interactively.

## Connecting it to Claude Desktop / Claude Code

Add this to your MCP client config (for Claude Desktop:
`~/Library/Application Support/Claude/claude_desktop_config.json` on
macOS, `%APPDATA%\Claude\claude_desktop_config.json` on Windows):

```json
{
  "mcpServers": {
    "github-issues": {
      "command": "/absolute/path/to/.venv/bin/python",
      "args": ["/absolute/path/to/github-mcp-server/server.py"],
      "env": {
        "GITHUB_TOKEN": "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        "GITHUB_DEFAULT_REPO": "your-username/your-repo"
      }
    }
  }
}
```

Restart the client, then in a conversation try: *"List the open
issues in my repo"* (read, runs immediately) or *"Open an issue
titled 'Fix login bug'"* (write — you'll see the preview + be asked
to confirm before it actually posts to GitHub).

For **Claude Code**, run `claude mcp add` and point it at the same
command/args, or add an equivalent entry to your project's
`.mcp.json`.

## Design notes worth highlighting (e.g. in an interview / CV writeup)

- **Least-privilege by default**: reads need no confirmation; writes
  always do. `REQUIRE_CONFIRMATION` can be flipped off for local
  testing but defaults to `true`.
- **Confirmation can't be forged**: the token is a server-generated
  fingerprint of `tool_name + args`, so a model can't just invent a
  token or reuse one from a different action.
- **Small, typed tool surface**: each tool has a docstring the LLM
  reads to decide when/how to call it — treat these docstrings as
  part of the API contract, not just documentation.
- **Payload trimming**: `_format_issue()` strips GitHub's verbose
  response down to what an LLM actually needs, keeping context usage
  low and avoiding leaking noisy internal fields.

## Extending it

The structure is deliberately generic — add a `notion_client.py`
alongside `github_client.py`, register new `@mcp.tool()` functions in
`server.py`, and reuse the same `confirmation.py` gate for any new
write actions (e.g. `create_notion_page`).

## Cost

Free — this only uses the free GitHub REST API within standard rate
limits (5,000 requests/hour for authenticated requests).

## Verification — proof it works end to end

This isn't just code that compiles — it's been run against a live
Claude Desktop client and a real (test) GitHub repo.

**1. Server boots and registers all 6 tools cleanly:**
```
$ python server.py
[hangs on stdio, no errors — waiting for a client]
```
Confirmed via Claude Desktop → Settings → Developer, where the
server shows a `Running` status with its command/args resolved
correctly.

**2. Read path — `list_issues` called live through Claude Desktop:**

Prompt: *"List the open issues in my mcp-server-test repo"*

Response: *"Your mcp-server-test repo (Rhytham4306/mcp-server-test)
has no open issues right now — it's all clear."*

Confirms the full chain: Claude → MCP client → this server (stdio)
→ GitHub REST API → back through the same path → correct answer.

**3. Write path with confirmation — `create_issue` called live:**

Prompt: *"Create an issue in mcp-server-test titled 'Test issue from
MCP' with body 'Testing write confirmation flow'"*

What actually happened under the hood (visible by expanding the tool
call in Claude Desktop): two separate calls to `create_issue` —

1. First call, no `confirm_token` → server returned a preview
   (`status: confirmation_required`) plus a single-use token. No
   write happened yet.
2. Second call, same arguments plus the returned `confirm_token` →
   server validated the token against the argument fingerprint,
   consumed it, and only then called the GitHub API.

Result: issue #1 was created at
`github.com/Rhytham4306/mcp-server-test/issues/1`.

**4. Confirmation gate rejects forged/mismatched tokens** — verified
directly by calling `create_issue` with a random, invalid
`confirm_token` string: the server returned
`{"status": "error", "error": "Confirmation token is invalid or has
expired..."}` and made no GitHub API call. See `confirmation.py` —
tokens are single-use, expire after 5 minutes, and are bound to a
SHA-256 fingerprint of the exact tool name + arguments, so a token
from one action can't be replayed against another.

### Screenshots
:
![Server running in Claude Desktop](screenshots-server-running.png)
![Live read call](screenshots-list-issues-demo.png)
![Live write call with confirmation](screenshots-server-running.png)
```