This guide walks you through building a GitHub MCP app using FastMCP and registering it with Orceum using OAuth authentication.

What We’re Building

An MCP server that:
  • Exposes GitHub tools (create_issue, list_issues, search_repos)
  • Uses GitHub OAuth for per-user authentication
  • Auto-discovers tools via Orceum’s MCP integration

Step 1: Build Your MCP Server

Install FastMCP:
pip install fastmcp httpx
# server.py
from fastmcp import FastMCP
import httpx
import os

mcp = FastMCP("GitHub Tools")

# In a real app, get the token from the request context
# FastMCP handles OAuth token injection per-request
GITHUB_API = "https://api.github.com"


@mcp.tool()
async def create_issue(
    repo: str,
    title: str,
    body: str = "",
    labels: list[str] = []
) -> dict:
    """
    Create a new GitHub issue in a repository.

    Use when the user wants to report a bug, request a feature, or track work
    in a specific GitHub repository.

    Args:
        repo: Repository in 'owner/repo' format, e.g. 'acme/todo-app'
        title: Short, descriptive issue title
        body: Detailed description of the issue (optional)
        labels: List of label names to apply, e.g. ['bug', 'high-priority']
    """
    async with httpx.AsyncClient() as client:
        resp = await client.post(
            f"{GITHUB_API}/repos/{repo}/issues",
            json={"title": title, "body": body, "labels": labels},
            headers={"Authorization": f"Bearer {get_token()}"}
        )
        resp.raise_for_status()
        data = resp.json()
        return {
            "issue_number": data["number"],
            "url": data["html_url"],
            "title": data["title"],
            "status": "created"
        }


@mcp.tool()
async def list_issues(
    repo: str,
    state: str = "open",
    limit: int = 10
) -> list:
    """
    List issues in a GitHub repository.

    Use when the user wants to see open bugs, check pending work, or review
    issues in a repository.

    Args:
        repo: Repository in 'owner/repo' format
        state: Filter by issue state: 'open', 'closed', or 'all'. Default: 'open'
        limit: Maximum number of issues to return (1–50). Default: 10
    """
    async with httpx.AsyncClient() as client:
        resp = await client.get(
            f"{GITHUB_API}/repos/{repo}/issues",
            params={"state": state, "per_page": min(limit, 50)},
            headers={"Authorization": f"Bearer {get_token()}"}
        )
        resp.raise_for_status()
        issues = resp.json()
        return [
            {"number": i["number"], "title": i["title"], "url": i["html_url"]}
            for i in issues
        ]


@mcp.tool()
async def search_repos(query: str, limit: int = 5) -> list:
    """
    Search for GitHub repositories matching a query.

    Use when the user wants to find repositories by name, topic, or description.

    Args:
        query: Search keywords, e.g. 'fastapi authentication python'
        limit: Number of results to return (1–20). Default: 5
    """
    async with httpx.AsyncClient() as client:
        resp = await client.get(
            f"{GITHUB_API}/search/repositories",
            params={"q": query, "per_page": min(limit, 20)},
            headers={"Authorization": f"Bearer {get_token()}"}
        )
        resp.raise_for_status()
        return [
            {
                "name": r["full_name"],
                "description": r["description"],
                "stars": r["stargazers_count"],
                "url": r["html_url"]
            }
            for r in resp.json()["items"]
        ]


def get_token():
    # In production: extract from request context (Orceum injects it)
    return os.environ.get("GITHUB_TOKEN", "")


if __name__ == "__main__":
    mcp.run(transport="streamable-http", host="0.0.0.0", port=8080)

Step 2: Deploy Your MCP Server

Your MCP server needs to be publicly accessible. Deploy it to any cloud provider:
# Example: deploy to Railway
railway up

# Your server URL will be something like:
# https://github-mcp.up.railway.app/mcp

Step 3: Register with Orceum

Go to the Orceum Developer Studio, click Create App, and select MCP App.
  1. Basic Info: Name it “GitHub Tools” and provide a description.
  2. Authentication: Select OAuth 2.0.
  3. MCP Details: Enter your deployment URL (e.g., https://github-mcp.up.railway.app/mcp) and select STREAMABLE_HTTP as the transport.
  4. OAuth Configuration: Enter your GitHub Client ID, Client Secret, and scopes (repo, read:user).
Once created, copy your new App ID. You must register the Orceum callback URI (https://api.orceum.com/v1/apps/{app_id}/oauth/callback) in your GitHub OAuth App settings.

Step 4: Verify Tool Discovery

After registration, Orceum will attempt to connect to your MCP server and run list_tools(). Check the Tools tab in the Developer Studio for your app. You should see your three tools (create_issue, list_issues, search_repos) automatically discovered. If you see zero tools, check that your server is reachable and running correctly.

Step 5: Test It

Install the app as a user and try it in Orceum:
“Find Python web framework repositories on GitHub”
“Create an issue in acme/backend titled ‘Fix login timeout‘“

Updating Your Tools

When you add or modify tools in your Python code and redeploy your server, you must refresh the manifest in Orceum. Go to the Tools tab in the Developer Studio and click Refresh Tools.

Key Differences from Native Apps

AspectNativeMCP
CreationDefine manifest and endpoints manuallyAuto-discovered via MCP endpoint
Actions definedIn Developer Studio manifest JSONVia Python tool decorators (list_tools())
ExecutionOrceum sends HTTP POSTOrceum calls call_tool(name, args)
Auth supportNone, API Key, OAuthNone, OAuth only
UpdatesManually update manifest JSONClick Refresh Tools