Mastering Python MCP Servers: A Practical GitHub API Integration Guide
Back to Blog
Backend12 min read

Mastering Python MCP Servers: A Practical GitHub API Integration Guide

HHazrat Ummar ShaikhJune 20, 20264 views

I've spent countless hours wrestling with AI models that, while brilliant, suffered from a debilitating lack of real-world context. Imagine a sophisticated code-generation AI trying to help with a pull request without knowing the project's architecture, recent commits, or even the issue it's trying to solve. The output was often generic, sometimes outright wrong, and always required significant human intervention. It felt like asking a world-class chef to cook without telling them what ingredients were in the pantry.

This frustration led me down a rabbit hole of custom context injection, hacky prompt engineering, and bespoke data pipelines. That was until I encountered the Model Context Protocol (MCP). What started as a promising Anthropic project has rapidly evolved, in my view, into an industry-standard for providing structured, real-time context to AI models. It’s a game-changer for moving AI from theoretical playgrounds to practical, production-ready systems.

Today, I want to walk you through building a high-performance Python MCP server from scratch, using FastAPI, and integrating it with the GitHub API. This isn't just an academic exercise; we're talking about empowering your AI agents with a deep understanding of your codebase, issues, and development workflows. You'll learn how to build a server that can fetch relevant code snippets, issue descriptions, pull request comments, and more, formatting it all into a clean, consumable MCP payload.

Why MCP Matters for Production AI

In the evolving landscape of AI, models are only as good as the data they consume. While large language models (LLMs) possess vast general knowledge, their utility in specific domains, like software development, plummets without relevant, real-time context. This is where MCP shines. It provides a standardized way to:

  • Reduce Hallucinations: By providing accurate, verified context, you significantly reduce the chances of the AI generating false or misleading information.
  • Improve Relevance: Tailor AI responses to the exact problem at hand, making them far more actionable.
  • Streamline Development: Move away from brittle, ad-hoc context injection methods to a robust, protocol-driven approach.
  • Scalability: Design modular context providers that can be easily extended to new data sources without refactoring your core AI integration.

From my experience building custom Discord bots that interact with various APIs (like what I detailed in Building a Discord Ticket Bot with Python), I've learned the critical importance of a clear, well-defined data flow. MCP provides exactly that for AI context.

Choosing the Right Tools: Python & FastAPI

For our MCP server, Python is an obvious choice. Its rich ecosystem, particularly for data processing and API development, makes it incredibly versatile. When it comes to building high-performance APIs in Python, my go-to has always been FastAPI. Why FastAPI?

  • Blazing Fast: Built on Starlette and Pydantic, it's incredibly performant, especially for asynchronous operations. In fact, when I evaluated backend frameworks for a recent project, FastAPI consistently outshone many alternatives in throughput, a topic I covered in my Ktor vs. FastAPI: A Backend Performance Comparison.
  • Asynchronous Support: Essential for I/O-bound tasks like making multiple GitHub API calls.
  • Data Validation & Serialization: Pydantic models automatically handle request validation and response serialization, drastically reducing boilerplate and potential errors.
  • OpenAPI/Swagger UI: Automatic interactive API documentation is a huge win for developer experience.

Core MCP Server Architecture

Our server will act as an intermediary:

  1. Receive an MCP context request.
  2. Identify the requested context type (e.g., 'github_repo', 'github_issue').
  3. Fetch the necessary data from the GitHub API.
  4. Format this data into a structured MCP payload.
  5. Return the payload to the requesting AI model or agent.
Isometric 3D rendering of a stylized glowing metallic python coiled around a high-speed server rack, with abstract binar

Setting Up Your FastAPI Project

First, let's get the basic FastAPI application running. Create a new directory and install the necessary packages:

mkdir mcp-github-server
cd mcp-github-server
pip install fastapi uvicorn "python-dotenv[dotenv]" httpx pydantic

Next, create a main.py file:

# main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
import httpx
import os
from dotenv import load_dotenv

load_dotenv()
app = FastAPI(title="GitHub MCP Context Server")
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
if not GITHUB_TOKEN:
    raise RuntimeError("GITHUB_TOKEN environment variable not set.")

GITHUB_API_BASE_URL = "https://api.github.com"

async def fetch_github_api(path: str, headers: dict = None, params: dict = None):
    _headers = {"Authorization": f"token {GITHUB_TOKEN}", "Accept": "application/vnd.github.v3+json"}
    if headers:
        _headers.update(headers)
    async with httpx.AsyncClient() as client:
        response = await client.get(f"{GITHUB_API_BASE_URL}{path}", headers=_headers, params=params)
        response.raise_for_status() # Raise an exception for bad status codes
        return response.json()

class MCPContextRequest(BaseModel):
    context_type: str = Field(..., description="Type of context requested, e.g., 'github_repo', 'github_issue'")
    params: dict = Field(default_factory=dict, description="Parameters for fetching context, e.g., {'owner': 'octocat', 'repo': 'Spoon-Knife'}")

class MCPContextResponse(BaseModel):
    context_id: str = Field(..., description="Unique ID for this context response")
    context: list[dict] = Field(..., description="List of context blocks conforming to MCP schema")

@app.get("/health")
async def health_check():
    return {"status": "ok", "message": "MCP GitHub Server is running"}

You'll also need a .env file in your project root for your GitHub Personal Access Token:

GITHUB_TOKEN="ghp_yourPersonalAccessTokenHere" # Replace with your actual token

Important: Make sure your GitHub token has the necessary read permissions for repositories, issues, and pull requests. Never expose your token directly in code or commit it to version control.

Integrating the GitHub API: Fetching Context

Now, let's implement the logic to fetch specific GitHub context. We'll start with a generic context provider and then specific handlers.

# main.py (continued)
class GitHubContextProvider:
    def __init__(self, token: str):
        self.token = token

    async def get_repo_context(self, owner: str, repo: str) -> dict:
        try:
            repo_data = await fetch_github_api(f"/repos/{owner}/{repo}")
            return {
                "type": "github_repository",
                "title": repo_data.get("full_name"),
                "url": repo_data.get("html_url"),
                "description": repo_data.get("description"),
                "stars": repo_data.get("stargazers_count"),
                "forks": repo_data.get("forks_count"),
                "language": repo_data.get("language"),
                "default_branch": repo_data.get("default_branch"),
                "topics": repo_data.get("topics", []),
                "content": f"Repository: {repo_data.get('full_name')}. Description: {repo_data.get('description', 'N/A')}"
            }
        except httpx.HTTPStatusError as e:
            if e.response.status_code == 404:
                raise HTTPException(status_code=404, detail=f"Repository {owner}/{repo} not found.")
            raise HTTPException(status_code=e.response.status_code, detail=f"GitHub API error: {e.response.text}")

    async def get_issue_context(self, owner: str, repo: str, issue_number: int) -> dict:
        try:
            issue_data = await fetch_github_api(f"/repos/{owner}/{repo}/issues/{issue_number}")
            comments_data = await fetch_github_api(f"/repos/{owner}/{repo}/issues/{issue_number}/comments")
            comment_summaries = [
                f"Comment by {c.get('user', {}).get('login')}: {c.get('body')}"
                for c in comments_data[:3]
            ]
            return {
                "type": "github_issue",
                "title": issue_data.get("title"),
                "url": issue_data.get("html_url"),
                "state": issue_data.get("state"),
                "author": issue_data.get("user", {}).get("login"),
                "body": issue_data.get("body"),
                "labels": [label.get("name") for label in issue_data.get("labels", [])],
                "comments_summary": comment_summaries,
                "content": f"Issue #{issue_number}: {issue_data.get('title')}. Status: {issue_data.get('state')}. Body: {issue_data.get('body', 'N/A')}. Comments: {'; '.join(comment_summaries)}"
            }
        except httpx.HTTPStatusError as e:
            if e.response.status_code == 404:
                raise HTTPException(status_code=404, detail=f"Issue #{issue_number} in {owner}/{repo} not found.")
            raise HTTPException(status_code=e.response.status_code, detail=f"GitHub API error: {e.response.text}")

github_provider = GitHubContextProvider(GITHUB_TOKEN)

@app.post("/mcp/context", response_model=MCPContextResponse)
async def provide_mcp_context(request: MCPContextRequest):
    context_blocks = []
    context_id = f"mcp-{request.context_type}-{hash(frozenset(request.params.items()))}"
    if request.context_type == "github_repo":
        owner = request.params.get("owner")
        repo = request.params.get("repo")
        if not owner or not repo:
            raise HTTPException(status_code=400, detail="'owner' and 'repo' are required for github_repo context.")
        repo_context = await github_provider.get_repo_context(owner, repo)
        context_blocks.append(repo_context)
    elif request.context_type == "github_issue":
        owner = request.params.get("owner")
        repo = request.params.get("repo")
        issue_number = request.params.get("issue_number")
        if not owner or not repo or not issue_number:
            raise HTTPException(status_code=400, detail="'owner', 'repo', and 'issue_number' are required for github_issue context.")
        issue_context = await github_provider.get_issue_context(owner, repo, int(issue_number))
        context_blocks.append(issue_context)
    else:
        raise HTTPException(status_code=400, detail=f"Unknown context_type: {request.context_type}")
    return MCPContextResponse(context_id=context_id, context=context_blocks)

Detailed high-tech concept illustration of a developer's workspace with multiple screens displaying FastAPI code and Git

Running the Server

To run your server, use Uvicorn:

uvicorn main:app --reload

You can then interact with it via http://127.0.0.1:8000/docs for the interactive OpenAPI documentation.

An example request to fetch repository context would look like this (using curl):

curl -X 'POST' \  'http://127.0.0.1:8000/mcp/context' \  -H 'accept: application/json' \  -H 'Content-Type: application/json' \  -d '{    "context_type": "github_repo",    "params": {      "owner": "octocat",      "repo": "Spoon-Knife"    }  }'

Handling Rate Limits and Pagination

The GitHub API has strict rate limits. For production systems, you must implement robust handling:

  • Exponential Backoff: If a request hits a rate limit (HTTP 403 or 429), retry after an increasing delay. The httpx library or a wrapper can help here.
  • Monitor Headers: GitHub sends X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers. Use these to proactively manage your requests.
  • Pagination: For resources like comments or large lists, GitHub APIs are paginated. Our simple example only fetches the first page. A production-ready solution would iterate through pages, perhaps with a configurable limit to prevent overwhelming the context payload.

These considerations are crucial for any system that heavily relies on external APIs. Even when debugging complex native Android OS internals, as I described in Demystifying Android OS Internals, understanding how different components interact and handle resource constraints is key to building stable software.

Deployment Considerations

Once your MCP server is ready, you'll need to deploy it. For a Python FastAPI application, popular choices include:

  • Docker: Containerizing your application provides consistency across environments.
  • VPS (Virtual Private Server): Providers like Vultr or DigitalOcean offer flexible, cost-effective options for deploying custom backend services. I often recommend Vultr for its balance of performance and competitive pricing, especially for developers who need full control over their stack. You can easily set up a Linux instance, install Docker, and deploy your FastAPI container.
  • Managed Platforms: Cloud run (GCP), App Runner (AWS), or similar serverless containers offer easier scaling but might have higher costs for consistent workloads.

Always ensure your GITHUB_TOKEN is injected securely as an environment variable and not hardcoded into your deployment artifacts.

Isometric 3D rendering of interconnected data nodes forming a complex network, representing context flowing from various

MCP and Other Context Handling Approaches: A Comparison

Before MCP, developers often resorted to custom JSON structures or ad-hoc string formatting for context. Let's compare the general approaches:

FeatureRaw String/Ad-hoc JSONCustom ProtocolModel Context Protocol (MCP)
StandardizationNoneInternal onlyIndustry standard, evolving
InteroperabilityLow (requires custom parsing per model)Low (specific to internal systems)High (models can be built to natively understand MCP)
MaintainabilityLow (fragile, changes break models)Medium (better than ad-hoc, but proprietary)High (protocol stability, clear schema)
Developer OverheadHigh (context formatting, validation)Medium (designing & implementing schema)Low (focus on data fetching, not schema design)
FlexibilityHigh (but chaotic)Medium to HighHigh (extensible context types)
Error HandlingManual, error-proneCustom implementationClear structure, easier validation

FAQ: Model Context Protocol (MCP) Server

Q1: What are the performance implications of fetching large GitHub contexts?

A: Fetching large contexts, especially from external APIs like GitHub, can introduce significant latency. The key is to be selective: only fetch the context absolutely necessary for the AI's current task. Implement caching strategies (e.g., Redis) for frequently accessed, static contexts. Utilize asynchronous HTTP clients (like httpx in our example) to prevent I/O blocking. Additionally, be mindful of GitHub API rate limits; exceeding them will lead to temporary service disruptions.

Q2: How do I handle GitHub API rate limits effectively in a production MCP server?

A: Robust rate limit handling is critical. Always include an Authorization header with your personal access token to get a higher rate limit (5000 requests per hour). Monitor the X-RateLimit-Remaining and X-RateLimit-Reset HTTP headers in GitHub's responses. If you hit a limit, use an exponential backoff strategy with jitter before retrying. Consider a distributed rate limiter if you have multiple instances of your MCP server. For extremely high-volume scenarios, a GitHub App installation token can offer even higher limits.

Q3: Can MCP be used with other APIs besides GitHub?

A: Absolutely! The power of MCP lies in its protocol-agnostic nature regarding context sources. Our GitHub integration is just one example. You can easily extend your MCP server to include context providers for internal knowledge bases (e.g., Confluence, Notion), project management tools (Jira, Trello), documentation systems, databases, or any other data source relevant to your AI's domain. Each context provider would simply fetch data from its respective API and format it into an MCP-compliant block.

Q4: What's the best way to secure MCP server endpoints?

A: Securing your MCP server is paramount, especially if it handles sensitive internal data. Implement API key authentication or OAuth 2.0 to ensure only authorized AI agents or internal services can request context. Use HTTPS for all communication to encrypt data in transit. If the server is exposed to the public internet, ensure proper firewall rules are in place, and consider integrating with an API Gateway for additional security layers like DDoS protection, request throttling, and unified authentication. Never hardcode API tokens or sensitive credentials directly in your application code; use environment variables or a secure secret management service.

Conclusion

Building a Python MCP server integrated with the GitHub API provides a robust foundation for building truly intelligent AI assistants and tools that understand the nuances of your development ecosystem. By adhering to the Model Context Protocol, you're not just throwing data at an AI; you're speaking its language, providing it with the structured, timely context it needs to deliver exceptional results. This approach will save you countless hours of debugging, improve the quality of your AI's output, and ultimately accelerate your development cycles. Start experimenting, extend your context providers, and watch your AI agents become indispensable members of your team.

Need Help with Custom APIs or Backend Systems?

I build robust, secure, and scalable backend services, databases, and microservices using FastAPI, Ktor, Node.js, and MongoDB. Let's build your server infrastructure!

H

Written by

Hazrat Ummar Shaikh

Android Developer with 4+ years of experience. Built production Android apps, Ktor backends, Discord bots, and SaaS products using Kotlin, Python, and MongoDB. Passionate about building robust systems and writing clean code.

Related Posts

Building a Scalable Python MCP Server for GitHub API Automation
Backend

The Model Context Protocol is now standard for AI. I'll guide you through building a high-performance Python MCP server for GitHub API automation.

#python#ai#mcp
Jun 20, 2026
Read More
Automating ITR Filings: A Python Deep Dive Saving 209 Hours
Backend

A weekend Python script I engineered saved a CA firm 209 hours during ITR season. I'll break down the FastAPI, MongoDB, and automation strategies that unlocked this massive efficiency gain.

#python#automation#fintech
Jun 20, 2026
Read More
Automating ITR Filings: A Python Script's 209-Hour Efficiency Gain
Backend

Discover how a lean Python script slashed 209 hours from ITR season workflows at a CA firm, showcasing the raw power of focused automation. This isn't just theory; it's a deep dive into practical financial process optimization.

#python#automation#fintech
Jun 20, 2026
Read More