Every time you connect an AI agent to an external API, you inherit the boring, risky part: OAuth flows, token storage, token refresh, and making sure nothing leaks.
In this tutorial I built a small MCP server in Python that exposes GitHub to any MCP client, where my code never sees a GitHub token. Nango handles the auth. My server only knows a Nango secret key and a connection ID.
Code: https://github.com/sravya520/nango-github-mcp
How it works
MCP client -> my MCP server (Python) -> Nango proxy (adds GitHub auth) -> GitHub API
The server exposes three tools:
| Tool | What it does |
|---|---|
list_my_repos |
Lists my repos, most recently updated first |
list_open_issues |
Lists open issues in a repo (skips pull requests) |
create_issue |
Creates an issue in a repo |
Step 1: Connect GitHub in Nango
Nango's getting-started flow creates a GitHub integration (github-getting-started) and has you authorize your GitHub account. That gives you a connection ID. From then on, Nango stores and refreshes the GitHub token for that connection.
Put your values in a .env file and add .env to .gitignore:
NANGO_SECRET_KEY=your-secret-key
NANGO_CONNECTION_ID=your-connection-id
NANGO_PROVIDER_CONFIG_KEY=github-getting-started
Step 2: Call GitHub through Nango's proxy
Instead of calling api.github.com with a token, you call Nango's proxy with three headers. Nango finds the connection, adds the GitHub token and forwards the request.
def nango_request(method, endpoint, params=None, json=None):
headers = {
"Authorization": f"Bearer {os.getenv('NANGO_SECRET_KEY')}",
"Connection-Id": os.getenv("NANGO_CONNECTION_ID"),
"Provider-Config-Key": os.getenv("NANGO_PROVIDER_CONFIG_KEY"),
}
response = httpx.request(
method, f"https://api.nango.dev/proxy{endpoint}",
headers=headers, params=params, json=json, timeout=20,
)
...
A one-line smoke test (GET /user) confirms the whole chain works:
Step 3: Turn the calls into MCP tools
I used the official MCP Python SDK. One thing that caught me out: in version 2.x, FastMCP was renamed to MCPServer, so older tutorials fail on import.
from mcp.server.mcpserver import MCPServer
mcp = MCPServer("github-via-nango")
@mcp.tool(annotations=READ_ONLY)
def list_my_repos(limit: int = 10) -> list[dict]:
"""List the user's GitHub repositories, most recently updated first.
Use this first to find a repo's full name (owner/name)."""
repos = nango_request("GET", "/user/repos",
params={"sort": "updated", "per_page": _clamp(limit)})
return [{"full_name": r["full_name"], "url": r["html_url"]} for r in repos]
Three small choices make agents behave better:
- The docstring is the instruction. The agent reads it to decide when to call the tool. "Use this first to find a repo's full name" helps it chain tools in the right order.
- Small results. Lists are capped at 20 items so the agent's context stays small.
-
Tool annotations. Read tools are marked read-only and
create_issueis not, so clients can treat writes more carefully.
Step 4: Make errors readable for the agent
This was my biggest lesson. In the MCP SDK, if a tool raises a normal Python exception, the agent only sees "Error executing tool". It has no idea what went wrong.
Only errors raised as ToolError pass their message through. So my error class extends it:
from mcp.server.mcpserver.exceptions import ToolError
class NangoError(ToolError):
"""A readable error the agent can show to the user."""
Now a bad repo name produces an answer the agent can act on (see the screenshot in the next step). I wrote a test that locks this in, plus seven others covering the Nango headers and URL, filtering out pull requests, input validation and error messages.
Step 5: Test it over real MCP
VS Code is an MCP host. When I added the server, it started it and showed it as Running, with all three tools:
To test the protocol directly, I wrote a small Python client (client_demo.py in the repo). It launches the server over stdio, lists the tools, and calls them the way an agent would:
And the error case, where the message comes through clearly:
What I tested, and what I didn't: list_my_repos ran live over MCP, through Nango to GitHub, and the bad-repo error came back over MCP too. list_open_issues and create_issue are covered by the unit tests, but I did not run them against my live account.
Problems I hit (so you don't have to)
-
401 Unauthorized from Nango. Nango has separate environments, each with its own secret key, so copy the key from the environment where your connection lives. Also check your
.envfor stray lines:python-dotenvwarned me about a parse error on line 4. -
"Command not found" in the VS Code MCP setup.
commandis the program that runs the server (Python). The server file goes inargs. I had typed the server's name intocommand. -
Wiring an AI client is the fiddly part. Copilot's agent mode was out of quota, and Claude Code's panel did not pick up my server even though
claude mcp listshowed it connected. Rather than keep debugging, I tested with the script above. Because the server speaks standard MCP, it should work with any MCP client.
Why I like this approach
The agent side stays simple: three small tools and clear errors. All the hard auth work (OAuth, storage, refresh) lives in Nango. Adding another API like Notion or Slack would mostly mean a new integration in Nango and a few more tools, not a new auth system.
Code, tests and setup: https://github.com/sravya520/nango-github-mcp




Top comments (0)