From 2fb67e774ca906254b4eabe5dfca59710a203a3d Mon Sep 17 00:00:00 2001 From: Foxbolts Date: Fri, 30 Oct 2026 12:20:47 -0400 Subject: [PATCH 1/3] Add local read-only GitHub MCP server setup --- docs/hackbot/github-mcp/check.py | 66 +++++++++++++++++++++++++++++ docs/hackbot/github-mcp/compose.yml | 6 +++ docs/hackbot/github.md | 45 ++++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 docs/hackbot/github-mcp/check.py create mode 100644 docs/hackbot/github-mcp/compose.yml create mode 100644 docs/hackbot/github.md diff --git a/docs/hackbot/github-mcp/check.py b/docs/hackbot/github-mcp/check.py new file mode 100644 index 00000000000..9a17793f7b8 --- /dev/null +++ b/docs/hackbot/github-mcp/check.py @@ -0,0 +1,66 @@ +"""Check the local GitHub MCP server against a pull request.""" + +import argparse +import asyncio +import os + +from mcp import ClientSession +from mcp.client.streamable_http import streamablehttp_client + + +async def check(url: str, token: str, owner: str, repo: str, number: int) -> None: + async with ( + streamablehttp_client(url, headers={"Authorization": f"Bearer {token}"}) as ( + read, + write, + _, + ), + ClientSession(read, write) as session, + ): + await session.initialize() + tools = (await session.list_tools()).tools + names = {tool.name for tool in tools} + non_read_only = [ + tool.name + for tool in tools + if not tool.annotations or not tool.annotations.readOnlyHint + ] + if non_read_only: + raise RuntimeError(f"Tools without read-only annotations: {non_read_only}") + if "pull_request_read" not in names: + raise RuntimeError("pull_request_read is missing") + + for method in ("get", "get_files"): + result = await session.call_tool( + "pull_request_read", + {"method": method, "owner": owner, "repo": repo, "pullNumber": number}, + ) + if result.isError: + raise RuntimeError(f"{method} failed: {result.content}") + print(f"\n{method}:\n") + for item in result.content: + if item.type == "text": + print(item.text) + + print(f"\nPASS: read {owner}/{repo}#{number}") + print(f"PASS: all {len(names)} tools have read-only annotations") + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--url", default="http://127.0.0.1:8082/") + parser.add_argument("--repo", required=True, help="repository in owner/name format") + parser.add_argument("--pr", type=int, required=True, help="pull request number") + args = parser.parse_args() + token = os.environ.get("GITHUB_MCP_TOKEN") + if not token: + parser.error("Set GITHUB_MCP_TOKEN to a GitHub token with read access") + try: + owner, repo = args.repo.split("/") + except ValueError: + parser.error("--repo must be owner/repository") + asyncio.run(check(args.url, token, owner, repo, args.pr)) + + +if __name__ == "__main__": + main() diff --git a/docs/hackbot/github-mcp/compose.yml b/docs/hackbot/github-mcp/compose.yml new file mode 100644 index 00000000000..034c4108030 --- /dev/null +++ b/docs/hackbot/github-mcp/compose.yml @@ -0,0 +1,6 @@ +services: + github-mcp: + image: ghcr.io/github/github-mcp-server:v2.0.3 + command: ["http", "--read-only", "--toolsets=repos,pull_requests,issues"] + ports: + - "127.0.0.1:8082:8082" diff --git a/docs/hackbot/github.md b/docs/hackbot/github.md new file mode 100644 index 00000000000..276fd9f1ad6 --- /dev/null +++ b/docs/hackbot/github.md @@ -0,0 +1,45 @@ +# GitHub MCP server for local development + +The official GitHub MCP server provides read-only repository, pull request, and +issue tools. The Compose setup runs it locally for development, independently of +any agent. Production deployment and agent integration are separate work. + +## Start the server + +Install Docker with Compose and start Docker, then run from the repository root: + +```bash +docker compose -f docs/hackbot/github-mcp/compose.yml up -d +``` + +The server listens at `http://127.0.0.1:8082/`. Clients running on the same +Compose network can use `http://github-mcp:8082/`. Each client must supply a +GitHub token in its `Authorization` header; the server does not hold the App's +private key. + +## Check a pull request + +The following uses the active GitHub CLI account to authenticate. Its token may +have more permissions than needed. Alternatively, set `GITHUB_MCP_TOKEN` to a +read-only installation token from a test GitHub App with access to the repository. + +```bash +export GITHUB_MCP_TOKEN="$(gh auth token)" +uv run --no-project --with 'mcp>=1,<2' python docs/hackbot/github-mcp/check.py \ + --repo OWNER/REPOSITORY --pr NUMBER +unset GITHUB_MCP_TOKEN +``` + +Replace `OWNER/REPOSITORY` and `NUMBER` with the pull request to check. The script +reads its details and files and checks that all exposed tools have read-only +annotations. Inspect the output to verify it matches the PR. + +The container's read-only flag controls its exposed tools. GitHub token +permissions also restrict what the token can do. The local server is not a +replacement for production access control. + +When finished: + +```bash +docker compose -f docs/hackbot/github-mcp/compose.yml down +``` From 3000b421e8909b0cc60c3d19bd56a76b2fcbcfd6 Mon Sep 17 00:00:00 2001 From: Foxbolts Date: Tue, 10 Nov 2026 10:25:57 -0500 Subject: [PATCH 2/3] move local GitHub MCP setup to services --- docs/hackbot/github.md | 6 +++--- pyproject.toml | 2 ++ {docs/hackbot => services}/github-mcp/check.py | 0 {docs/hackbot => services}/github-mcp/compose.yml | 0 4 files changed, 5 insertions(+), 3 deletions(-) rename {docs/hackbot => services}/github-mcp/check.py (100%) rename {docs/hackbot => services}/github-mcp/compose.yml (100%) diff --git a/docs/hackbot/github.md b/docs/hackbot/github.md index 276fd9f1ad6..8edc57597b9 100644 --- a/docs/hackbot/github.md +++ b/docs/hackbot/github.md @@ -9,7 +9,7 @@ any agent. Production deployment and agent integration are separate work. Install Docker with Compose and start Docker, then run from the repository root: ```bash -docker compose -f docs/hackbot/github-mcp/compose.yml up -d +docker compose -f services/github-mcp/compose.yml up -d ``` The server listens at `http://127.0.0.1:8082/`. Clients running on the same @@ -25,7 +25,7 @@ read-only installation token from a test GitHub App with access to the repositor ```bash export GITHUB_MCP_TOKEN="$(gh auth token)" -uv run --no-project --with 'mcp>=1,<2' python docs/hackbot/github-mcp/check.py \ +uv run --no-project --with 'mcp>=1,<2' python services/github-mcp/check.py \ --repo OWNER/REPOSITORY --pr NUMBER unset GITHUB_MCP_TOKEN ``` @@ -41,5 +41,5 @@ replacement for production access control. When finished: ```bash -docker compose -f docs/hackbot/github-mcp/compose.yml down +docker compose -f services/github-mcp/compose.yml down ``` diff --git a/pyproject.toml b/pyproject.toml index 2d196dda4f5..af4d0e6345d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -143,6 +143,8 @@ members = ["http_service", "agents/*", "libs/*", "services/*", "tools/*"] exclude = [ # JavaScript package (no pyproject.toml). "services/hackbot-ui", + # Compose configuration, not a Python package. + "services/github-mcp", # Not migrated to the workspace yet. "services/buildrepair", ] diff --git a/docs/hackbot/github-mcp/check.py b/services/github-mcp/check.py similarity index 100% rename from docs/hackbot/github-mcp/check.py rename to services/github-mcp/check.py diff --git a/docs/hackbot/github-mcp/compose.yml b/services/github-mcp/compose.yml similarity index 100% rename from docs/hackbot/github-mcp/compose.yml rename to services/github-mcp/compose.yml From 8a0d8ed6c7b427077adfa9175d19462e3b53f305 Mon Sep 17 00:00:00 2001 From: Foxbolts Date: Thu, 12 Nov 2026 14:46:20 -0500 Subject: [PATCH 3/3] Remove GitHub MCP manual checker --- docs/hackbot/github.md | 17 ---------- services/github-mcp/check.py | 66 ------------------------------------ 2 files changed, 83 deletions(-) delete mode 100644 services/github-mcp/check.py diff --git a/docs/hackbot/github.md b/docs/hackbot/github.md index 8edc57597b9..8962385dc69 100644 --- a/docs/hackbot/github.md +++ b/docs/hackbot/github.md @@ -17,23 +17,6 @@ Compose network can use `http://github-mcp:8082/`. Each client must supply a GitHub token in its `Authorization` header; the server does not hold the App's private key. -## Check a pull request - -The following uses the active GitHub CLI account to authenticate. Its token may -have more permissions than needed. Alternatively, set `GITHUB_MCP_TOKEN` to a -read-only installation token from a test GitHub App with access to the repository. - -```bash -export GITHUB_MCP_TOKEN="$(gh auth token)" -uv run --no-project --with 'mcp>=1,<2' python services/github-mcp/check.py \ - --repo OWNER/REPOSITORY --pr NUMBER -unset GITHUB_MCP_TOKEN -``` - -Replace `OWNER/REPOSITORY` and `NUMBER` with the pull request to check. The script -reads its details and files and checks that all exposed tools have read-only -annotations. Inspect the output to verify it matches the PR. - The container's read-only flag controls its exposed tools. GitHub token permissions also restrict what the token can do. The local server is not a replacement for production access control. diff --git a/services/github-mcp/check.py b/services/github-mcp/check.py deleted file mode 100644 index 9a17793f7b8..00000000000 --- a/services/github-mcp/check.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Check the local GitHub MCP server against a pull request.""" - -import argparse -import asyncio -import os - -from mcp import ClientSession -from mcp.client.streamable_http import streamablehttp_client - - -async def check(url: str, token: str, owner: str, repo: str, number: int) -> None: - async with ( - streamablehttp_client(url, headers={"Authorization": f"Bearer {token}"}) as ( - read, - write, - _, - ), - ClientSession(read, write) as session, - ): - await session.initialize() - tools = (await session.list_tools()).tools - names = {tool.name for tool in tools} - non_read_only = [ - tool.name - for tool in tools - if not tool.annotations or not tool.annotations.readOnlyHint - ] - if non_read_only: - raise RuntimeError(f"Tools without read-only annotations: {non_read_only}") - if "pull_request_read" not in names: - raise RuntimeError("pull_request_read is missing") - - for method in ("get", "get_files"): - result = await session.call_tool( - "pull_request_read", - {"method": method, "owner": owner, "repo": repo, "pullNumber": number}, - ) - if result.isError: - raise RuntimeError(f"{method} failed: {result.content}") - print(f"\n{method}:\n") - for item in result.content: - if item.type == "text": - print(item.text) - - print(f"\nPASS: read {owner}/{repo}#{number}") - print(f"PASS: all {len(names)} tools have read-only annotations") - - -def main() -> None: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--url", default="http://127.0.0.1:8082/") - parser.add_argument("--repo", required=True, help="repository in owner/name format") - parser.add_argument("--pr", type=int, required=True, help="pull request number") - args = parser.parse_args() - token = os.environ.get("GITHUB_MCP_TOKEN") - if not token: - parser.error("Set GITHUB_MCP_TOKEN to a GitHub token with read access") - try: - owner, repo = args.repo.split("/") - except ValueError: - parser.error("--repo must be owner/repository") - asyncio.run(check(args.url, token, owner, repo, args.pr)) - - -if __name__ == "__main__": - main()