Skip to main content
Glama
Rhytham4306

github-mcp-server

by Rhytham4306

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.

Related MCP server: GitHub Issue Manager

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:

    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:

    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:

    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):

{
  "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 Live read call Live write call with confirmation


Related MCP Connectors

Related MCP Servers