An MCP (Model Context Protocol) server written in Go that wraps the gh GitHub CLI.
It exposes GitHub operations required by the AI agent skills in ~/.agents/skills as purpose-built MCP tools,
delegating all execution to the gh CLI (no reimplementation of the GitHub API).
Built with github.com/mark3labs/mcp-go.
This server deliberately exposes no generic API wrappers (no gh_api or gh_api_graphql pass-through).
Each tool encapsulates a specific, concrete workflow derived from the skill files. The caller passes
business-level inputs (PR number, comment ID, reply body) — never API paths, HTTP methods, or GraphQL queries.
This ensures tools are self-documenting, type-safe, and map 1:1 to skill steps.
- Go 1.23+ (built/tested on Go 1.26)
ghCLI installed and authenticated (gh auth login)giton PATH (used implicitly by someghsubcommands such asgh pr checkout)
go build -o gh-mcp-server .The server speaks MCP over stdio:
./gh-mcp-server{
"mcpServers": {
"gh": {
"command": "/Users/Shared/Opensource/gh-mcp-server/gh-mcp-server"
}
}
}Every tool shells out to gh and returns stdout as an MCP text result. Non-zero gh exit codes
are surfaced as MCP error results (isError: true) with the gh stderr message. Multi-line markdown
bodies (PR/issue create, PR comment) are written to a temp file and passed via --body-file so
newlines are preserved. For REST/GraphQL tools that need owner/repo resolution, the GH_REPO env
var is set so gh api's {owner}/{repo} placeholders resolve correctly — the caller never
constructs API paths.
Every tool requires a cwd string argument: the filesystem path to run gh from. The
MCP server is launched once by the client and inherits the client's working directory, which is
usually not the project you're operating on. Without cwd, gh resolves the repository from
whatever git remote is reachable from the server's launch directory (often none), so PR/issue
commands fail or target the wrong repo. Always pass cwd set to the project root so gh reads the
correct git remote.
Every tool also requires a
cwdstring param — see Working directory above.
- Purpose: Load a pull request's metadata as JSON (number, title, body, branches, state, commits).
- Use case: pr-fix Step 2 — load PR context before analyzing feedback. pr-create — detect an existing PR for the current branch.
- Differs from generic API: caller passes a PR number/URL/branch, not an API endpoint. Returns structured JSON fields selected for the review-fix workflow, not raw REST output.
- gh mapping:
gh pr view <pr_number> --json <fields> [--repo OWNER/REPO] - Params:
pr_number(required),fields(default:number,title,body,url,headRefName,baseRefName,author,state,commits),repo
- Purpose: Check out a PR's head branch locally to align the working branch with the PR.
- Use case: pr-fix Step 3 — switch to the PR's branch before making fixes. The skill requires this even if the current branch looks related; the PR ref is authoritative.
- Differs from generic API: encapsulates the checkout workflow, not a raw git command. The caller doesn't need to know the branch name — just the PR number.
- gh mapping:
gh pr checkout <pr_number> [--repo OWNER/REPO] - Params:
pr_number(required),repo
- Purpose: Get the diff of a pull request against its base branch.
- Use case: pr-fix Step 4 — understand what the PR currently changes before deciding what review feedback to address.
- Differs from generic API: returns a PR-specific diff, not an arbitrary git diff. The caller passes a PR number, not a ref range.
- gh mapping:
gh pr diff <pr_number> [--patch ref] [--repo OWNER/REPO] - Params:
pr_number(required),patch,repo
- Purpose: Create a pull request for the current branch with a structured markdown body.
- Use case: pr-create Step 8 — open a PR with ticket link, description, and checklist. ship-changes Step 5 — via pr-create.
- Differs from generic API: handles the full PR creation workflow including temp-file body preservation and self-assignment. Caller passes title/body/base/head, not CLI flags.
- gh mapping:
gh pr create --title --body-file --base --head --assignee [--repo OWNER/REPO] - Params:
title(required),body(required, markdown),base,head,assignee(default@me),repo
- Purpose: Post a single flat (non-threaded) PR comment.
- Use case: pr-fix Step 12 — the sole exception for quote-replying to flat PR comments
that have no line anchor and thus no thread to reply to. For line-anchored review-thread replies,
use
gh_pr_reply_to_review_threadinstead. - Differs from generic API: narrowly scoped to the one permitted flat-comment scenario. The description explicitly warns against using it for thread replies.
- gh mapping:
gh pr comment <pr_number> --body-file [--repo OWNER/REPO] - Params:
pr_number(required),body(required),repo
- Purpose: List flat (non-threaded, non-line-anchored) PR comments — the standalone conversation
comments invisible to
gh_pr_list_review_comments. - Use case: pr-fix Step 5 — load general PR discussion, questions, or status notes that have no file/line context. Without this tool, standalone comments are invisible to the agent.
- Differs from
gh_pr_list_review_comments: queriespullRequest.comments(flat, no path/line, noisResolved) instead ofpullRequest.reviewThreads(line-anchored threads). The two are complementary — a PR can have both kinds. - gh mapping:
gh api graphqlwithpullRequest.comments(first:100) - Params:
pr_number(required),repo
- Purpose: List line-anchored review comments (review threads) on a PR with full metadata.
- Use case: pr-fix Steps 2 & 5 — load inline review comments with file/line/body/ID to categorize feedback, identify actionable threads, and extract comment IDs for thread replies.
- Differs from generic API: caller passes a PR number, not a GraphQL query. The query, variables, and placeholder resolution are encapsulated. Returns threaded GraphQL data (threads with id, isResolved, path, and comments with databaseId, body, author, path, line) — not a summary.
- gh mapping:
gh api graphqlwithpullRequest.reviewThreads(first:100).comments(first:100) - Params:
pr_number(required),repo
- Purpose: List only UNRESOLVED line-anchored review threads on a PR (filters
isResolved=falseserver-side). - Use case: pr-fix Step 5 — load only actionable threads, skipping already-resolved ones to save tokens and avoid re-addressing fixed feedback.
- Differs from
gh_pr_list_review_comments: same GraphQL query, but unresolved threads are filtered server-side before rendering. Smaller output. Pagination applies before the filter, so a page may return fewer unresolved threads thanfirst. - gh mapping:
gh api graphqlwithpullRequest.reviewThreads(first:100).comments(first:100), filtered toisResolved=false - Params:
pr_number(required),repo
- Purpose: List reviews (approve/request-changes/comment states) on a PR.
- Use case: pr-fix Step 5 — check review states (
CHANGES_REQUESTED,APPROVED, etc.) to determine which reviews still have open feedback requiring fixes. - Differs from generic API: caller passes a PR number, not a GraphQL query. Encapsulates the reviews query specifically — distinct from review comments. Returns GraphQL nodes with id, body, state, author, submittedAt.
- gh mapping:
gh api graphqlwithpullRequest.reviews(first:100) - Params:
pr_number(required),repo
- Purpose: Reply directly to a specific line-anchored review thread comment.
- Use case: pr-fix Step 12 — the ABSOLUTE RULE: always reply to the specific thread, never add standalone comments. The reply appears inline in the existing thread.
- Differs from generic API: caller passes
pr_number+comment_id+body, not a POST endpoint with a JSON payload. The endpoint construction, HTTP method, and JSON body encoding are encapsulated. The reply body is JSON-encoded and sent via stdin (--input -). - gh mapping:
gh api repos/{owner}/{repo}/pulls/{pr}/comments/{id}/replies --method POST --input - - Params:
pr_number(required),comment_id(required),body(required),repo
- Purpose: Fetch unresolved review threads with their GraphQL node IDs (
PRRT_...). - Use case: pr-fix Step 13 — map REST comment database IDs to GraphQL thread node IDs so addressed threads can be resolved. Thread resolution is GraphQL-only (no REST endpoint).
- Differs from generic API: caller passes a PR number, not a GraphQL query string. The query,
variable binding, and placeholder resolution are encapsulated. Returns threads with
id,isResolved, and first comment metadata. - gh mapping:
gh api graphql -f query=... -F owner={owner} -F repo={repo} -F pr=<n> - Params:
pr_number(required),repo
- Purpose: Resolve a review thread by its GraphQL node ID.
- Use case: pr-fix Step 13 — mark a thread as resolved after feedback is fully addressed and a reply has been posted. Only resolve threads where the fix is complete.
- Differs from generic API: caller passes a thread node ID, not a GraphQL mutation string.
The
resolveReviewThreadmutation is encapsulated. - gh mapping:
gh api graphql -f query='mutation { resolveReviewThread(input: {threadId: "..."}) { thread { isResolved } } }' - Params:
thread_id(required)
- Purpose: Create a GitHub issue with a structured markdown body.
- Use case: pr-create Step 6 — auto-create a ticket from summarized branch work when in
autoticket mode, with a generated title, description, and validation checklist. - Differs from generic API: handles the full issue creation workflow including temp-file body preservation and self-assignment. Caller passes title/body, not CLI flags.
- gh mapping:
gh issue create --title --body-file --assignee [--label] [--repo OWNER/REPO] - Params:
title(required),body(required, markdown),assignee(default@me),label,repo
- Purpose: Check gh CLI authentication status.
- Use case: pr-fix, pr-create — verify gh is authenticated before attempting PR or issue operations, since all other tools will fail if auth is missing or expired.
- Differs from generic API: surfaces auth diagnostics (including non-zero exit output) as readable text, not an error. A dedicated precondition check, not a generic shell-out.
- gh mapping:
gh auth status - Params: none
| Skill | Tools used |
|---|---|
| pr-fix | gh_auth_status (precondition) → gh_pr_view (Step 2) → gh_pr_checkout (Step 3) → gh_pr_diff (Step 4) → gh_pr_list_review_comments + gh_pr_list_reviews (Step 5) → gh_pr_reply_to_review_thread (Step 12) → gh_pr_list_review_threads + gh_pr_resolve_review_thread (Step 13); gh_pr_comment (sole flat-comment exception) |
| pr-create | gh_auth_status (precondition) → gh_issue_create (Step 6: auto-ticket) → gh_pr_create (Step 8) → gh_pr_view (detect existing PR) |
| ship-changes | orchestrates new-branch → commit-and-push → pr-create; reaches GitHub only via gh_pr_create (through pr-create) |
The first iteration exposed generic gh_api and gh_api_graphql pass-through tools. These were
removed because they violate the purpose-built principle: they required the caller to know API
paths, HTTP methods, and GraphQL query strings — the exact knowledge the MCP server should
encapsulate. The 5 specific tools that replaced them (gh_pr_list_review_comments,
gh_pr_list_reviews, gh_pr_reply_to_review_thread, gh_pr_list_review_threads,
gh_pr_resolve_review_thread) cover every gh api / gh api graphql use case found in the skill
files, with business-level inputs instead of API-level plumbing.
main.go entrypoint; starts stdio server
internal/gh/gh.go gh CLI runner (stdin, args, env, exit-code/error handling)
internal/mcpserver/server.go server creation + tool registration
internal/mcpserver/tools.go tool definitions (names, schemas, purpose descriptions)
internal/mcpserver/handlers.go tool handler implementations
Quick smoke test against the running server:
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"0.0.0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
| ./gh-mcp-serverTest a purpose-built REST tool (lists reviews on cli/cli PR #1):
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"0.0.0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"gh_pr_list_reviews","arguments":{"pr_number":"1","repo":"cli/cli"}}}' \
| ./gh-mcp-server