Create an MCP Server that uses the GitHub API—list repos, search issues, and create issues via Claude.
The GitHub API is one of the best-documented and most widely used REST APIs in the world. It lets you programmatically access practically everything you do in the web interface—repositories, issues, pull requests, commits, organizations. For our MCP Server, we’ll use this API as the foundation for building powerful tools Claude can use to interact with your GitHub projects.
The GitHub REST API is available at api.github.com. The main endpoints we'll use are: GET /user/repos to list your repositories, GET /repos/{owner}/{repo}/issues to search for issues in a specific repository, and POST /repos/{owner}/{repo}/issues to create new issues. All responses are in JSON format, making it easier to parse and format the data. The API has rate limits — 5,000 requests per hour with authentication — which your server must respect.
The GitHub API processes more than billions of requests per day and is used by virtually all modern development tools—CI/CD, IDEs, bots, dashboards. With token authentication, the rate limit increases to 5,000 requests/hour, more than enough for a personal MCP Server. Without authentication, the limit drops to just 60 requests/hour, making any practical use unfeasible.
Before writing any code, test the API manually with curl or with tools like Insomnia/Postman. Run curl https://api.github.com/users/{seu-usuario}/repos to see the structure of the returned JSON. This makes implementing the MCP Server tools much easier, since you’ll know exactly which fields to extract and format.
To access the GitHub API with authentication—which is essential for decent rate limits and accessing private repositories—you need a Personal Access Token (PAT). This token works like a temporary password with a limited scope, allowing your MCP Server to access the API on your behalf without needing your actual password.
To create a Personal Access Token: go to GitHub Settings → Developer settings → Personal access tokens → Fine-grained tokens. Select the required scopes—for our project, we need access to repos e issues. The generated token must be stored as an environment variable (e.g., GITHUB_TOKEN) and passed through HTTP headers as Authorization: Bearer {token}. Never put the token directly in the source code.
NEVER commit tokens to the repository! If you accidentally push a token to GitHub, GitHub Secret Scanning will automatically revoke it—but the damage may already be done. Always use environment variables, add .env to .gitignore, and never log the token value in server outputs.
The first tool in our GitHub MCP Server will list repositories. Using the endpoint GET /user/repos, we'll fetch the authenticated user's repositories, filter by visibility, sort by last updated, and format the results in a readable way for Claude to present to the user.
The repository listing tool uses the library httpx to make asynchronous requests to the GitHub API. The essential code:
@server.tool()
async def list_repos(visibility: str = "all") -> str:
"""Lista repositórios do usuário autenticado."""
async with httpx.AsyncClient() as client:
resp = await client.get(
"https://api.github.com/user/repos",
headers={"Authorization": f"Bearer {GITHUB_TOKEN}"},
params={"visibility": visibility, "sort": "updated"}
)
repos = resp.json()
lines = []
for r in repos:
stars = r["stargazers_count"]
lang = r.get("language", "N/A")
lines.append(f"⭐ {stars} | {r['name']} ({lang}) — {r.get('description', 'Sem descrição')}")
return "\n".join(lines)
Format the output so that the Claude can interpret easily. Include the name, description, primary language, and star count. Use optional parameters such as visibility (all, public, private) to give Claude flexibility in its queries. The parameter sort=updated ensures the most recent repos appear first.
Claude receives the user's request ("list my repos") and calls the list_repos tool with the appropriate parameters.
The MCP Server sends GET /user/repos with the authentication token and receives the list of repositories in JSON.
The tool formats the data (name, description, language, stars) and returns structured text for Claude to present.
The second tool lets you search for issues in a specific repository. Using the endpoint GET /repos/{owner}/{repo}/issues, Claude will be able to check open or closed issues, filter by labels, and present relevant information such as number, title, author, and assigned labels.
The issue search tool receives owner (repository owner), repo (repository name) and optional parameters such as state (open/closed/all) and labels (filter by labels). The endpoint returns a JSON array where each issue contains: number, title, user.login, state, labels[] e created_at. Format the output with the number, title, author, and labels to make it easier to read.
One important detail: the endpoint /repos/{owner}/{repo}/issues returns both issues and pull requests. To filter only real issues, check whether the field pull_request is missing from the object. The API supports pagination via parameters page e per_page (maximum 100 per page).
The third tool is the most powerful—and the most dangerous. Using POST /repos/{owner}/{repo}/issues, Claude will be able to create issues directly in your repository. This is a write action, which requires careful validation of parameters and awareness that we’re giving the model the ability to modify real data.
The issue creation tool sends a POST with a JSON body containing title (required) and body (description, optional but recommended). Validate that the title is not empty and has a reasonable length. After creating it, return the URL of the created issue so the user can access it directly:
@server.tool()
async def create_issue(owner: str, repo: str, title: str, body: str = "") -> str:
"""Cria uma nova issue em um repositório GitHub."""
if not title.strip():
return "Erro: o título da issue não pode ser vazio."
async with httpx.AsyncClient() as client:
resp = await client.post(
f"https://api.github.com/repos/{owner}/{repo}/issues",
headers={"Authorization": f"Bearer {GITHUB_TOKEN}"},
json={"title": title, "body": body}
)
if resp.status_code == 201:
issue = resp.json()
return f"Issue criada com sucesso! #{issue['number']}: {issue['html_url']}"
return f"Erro ao criar issue: {resp.status_code}"
Creating issues is a write action — it modifies real data in your GitHub. Unlike listing or searching, this tool has permanent side effects. Consider adding a confirmation before creating an issue, or at least logging each issue created. In a corporate environment, this tool should have strict access control and a complete audit trail.
Always return the URL of the created issue in the response—this allows Claude to show the user a clickable link. Validate the parameters before sending the request: nonempty title, and owner and repo in the correct format. Consider adding optional parameters such as labels e assignees to create more complete issues.
With the three tools implemented—list repos, search issues, and create issues—it's time to test the complete integration. Configure the server in Claude Desktop and try natural-language commands to verify that everything works correctly. Claude should be able to automatically choose the right tool based on your request.
Test with these prompts in Claude Desktop:
Ask Claude to list your repositories. Check whether the data matches what appears in the GitHub web interface.
Ask it to search for issues in a real repository. Confirm that the numbers, titles, and labels are correct.
Ask it to create a test issue. Check on GitHub that the issue was created with the correct title and body.
Ask it to search for issues in a repository that doesn't exist. Check whether the error is handled gracefully.
Next Module: 4.5 — Combining Multiple Servers