GitHub
Build an MCP server with GitHub API tools using Keycard delegated access
Build an MCP server with repository-focused tools that let AI agents interact with GitHub on behalf of users.
GitHub Setup
Section titled “GitHub Setup”-
Copy your Keycard redirect URL
In Keycard Console, navigate to Zone Settings and copy the OAuth2 Redirect URL. You will need this when creating the GitHub OAuth App.
-
Create a GitHub OAuth App
In GitHub Developer Settings, create a new OAuth App and set the Authorization callback URL to the redirect URL you copied from Keycard. Once created, note the Client ID and generate a Client Secret.
-
Add GitHub from Resource Catalog
In Keycard Console, navigate to Resource Catalog and add GitHub. Enter the Client ID and Client Secret from step 2. This creates the GitHub OAuth provider and API resource.
-
Register your MCP server Resource
Navigate to Resources → Create Resource:
Field Value Resource Name GitHub MCP Server Resource Identifier http://localhost:8000/mcpCredential Provider Zone Provider -
Create an Application
Navigate to Applications → Create Application:
Field Value Provided Resource GitHub MCP Server (your MCP server) Dependency GitHub API After creating the Application, generate client credentials (Client ID + Client Secret) and save them.
Implementation
Section titled “Implementation”The server is split into two files: setup and tools. The setup file configures Keycard authentication; the tools file contains your API wrappers.
Install dependencies:
pip install keycardai-fastmcp fastmcp httpxCreate a .env file:
KEYCARD_ZONE_ID=<your-zone-id>MCP_SERVER_URL=http://localhost:8000KEYCARD_CLIENT_ID=<your-client-id>KEYCARD_CLIENT_SECRET=<your-client-secret>server.py: Server setup and entry point:
"""GitHub MCP Server with Keycard Delegated Access."""
import os
from fastmcp import FastMCP
from keycardai.fastmcp import AuthProvider, ClientSecret
auth_provider = AuthProvider( zone_id=os.getenv("KEYCARD_ZONE_ID"), mcp_server_name="GitHub MCP Server", mcp_base_url=os.getenv("MCP_SERVER_URL", "http://localhost:8000"), application_credential=ClientSecret(( os.getenv("KEYCARD_CLIENT_ID"), os.getenv("KEYCARD_CLIENT_SECRET"), )),)
auth = auth_provider.get_remote_auth_provider()mcp = FastMCP("GitHub MCP Server", auth=auth)
from tools import register_tools # noqa: E402
register_tools(mcp, auth_provider)
def main(): mcp.run(transport="streamable-http")
if __name__ == "__main__": main()tools.py: Each tool uses the grant decorator and extracts the exchanged token:
"""GitHub tools: thin wrappers around the GitHub REST API."""
import httpxfrom fastmcp import Context, FastMCP
from keycardai.fastmcp import AccessContext, AuthProvider
GITHUB_API = "https://api.github.com"
async def github_request(access_context: AccessContext, method: str, path: str, **kwargs) -> dict: """Make an authenticated request to the GitHub API.""" token = access_context.access(GITHUB_API).access_token async with httpx.AsyncClient() as client: response = await client.request( method, f"{GITHUB_API}{path}", headers={ "Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json", }, **kwargs, ) response.raise_for_status() return response.json()
def register_tools(mcp: FastMCP, auth_provider: AuthProvider): @mcp.tool() @auth_provider.grant(GITHUB_API) async def list_repos(ctx: Context, per_page: int = 30, sort: str = "updated") -> dict: """List the authenticated user's repositories.""" access_context: AccessContext = await ctx.get_state("keycardai") if access_context.has_errors(): return {"error": "Token exchange failed", "details": access_context.get_errors()}
repos = await github_request( access_context, "GET", "/user/repos", params={"per_page": per_page, "sort": sort} ) return { "count": len(repos), "repositories": [ {"name": r["name"], "full_name": r["full_name"], "html_url": r["html_url"]} for r in repos ], }
# Additional tools follow the same pattern: grant → get_state → check errors → call API # See full examples: https://github.com/keycardai/python-sdk/tree/main/packages/mcp-fastmcp/examplesInstall dependencies:
npm install @keycardai/mcp @modelcontextprotocol/sdk expressnpm install -D typescript @types/expressCreate a .env file:
KEYCARD_ZONE_URL=https://<your-zone-id>.keycard.cloudKEYCARD_CLIENT_ID=<your-client-id>KEYCARD_CLIENT_SECRET=<your-client-secret>PORT=8080server.ts: Server setup and entry point:
import express from "express";import { mcpAuthMetadataRouter } from "@keycardai/mcp/server/auth/router";import { AuthProvider } from "@keycardai/mcp/server/auth/provider";import { ClientSecret } from "@keycardai/mcp/server/auth/credentials";import { registerGitHubRoutes } from "./tools";
const ZONE_URL = process.env.KEYCARD_ZONE_URL ?? "https://<your-zone-id>.keycard.cloud";const CLIENT_ID = process.env.KEYCARD_CLIENT_ID ?? "<your-client-id>";const CLIENT_SECRET = process.env.KEYCARD_CLIENT_SECRET ?? "<your-client-secret>";const PORT = Number(process.env.PORT ?? 8080);
const authProvider = new AuthProvider({ zoneUrl: ZONE_URL, applicationCredential: new ClientSecret(CLIENT_ID, CLIENT_SECRET),});
const app = express();app.use(express.json());
app.use( mcpAuthMetadataRouter({ oauthMetadata: { issuer: ZONE_URL }, resourceName: "GitHub MCP Server", }),);
registerGitHubRoutes(app, authProvider);
app.listen(PORT, () => { console.log(`GitHub MCP Server running on http://localhost:${PORT}`);});tools.ts: Each route uses the grant middleware and extracts the exchanged token:
import type { Express } from "express";import type { AuthProvider, DelegatedRequest } from "@keycardai/mcp/server/auth/provider";
const GITHUB_API = "https://api.github.com";
async function githubRequest(token: string, path: string, options?: RequestInit) { const response = await fetch(`${GITHUB_API}${path}`, { ...options, headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", ...options?.headers, }, }); if (!response.ok) throw new Error(`GitHub API error: ${response.status}`); return response.json();}
export function registerGitHubRoutes(app: Express, authProvider: AuthProvider) { app.get("/api/repos", authProvider.grant(GITHUB_API), async (req, res) => { const { accessContext } = req as DelegatedRequest; if (accessContext.hasErrors()) { res.status(502).json({ error: "Token exchange failed", details: accessContext.getErrors() }); return; } const token = accessContext.access(GITHUB_API).accessToken; const repos = await githubRequest(token, "/user/repos?per_page=30&sort=updated"); res.json({ count: repos.length, repositories: repos.map((r: any) => ({ name: r.name, full_name: r.full_name, html_url: r.html_url, })), }); });
// Additional routes follow the same pattern: grant → accessContext → check errors → call API // See full examples: https://github.com/keycardai/typescript-sdk/tree/main/examples}Test It
Section titled “Test It”-
Start your server
Terminal window python server.pyTerminal window npx tsx server.ts -
Configure your agent
Add the MCP server to your agent configuration:
Cursor: add to
.cursor/mcp.json:{"mcpServers": {"github-mcp": {"url": "http://localhost:8000/mcp"}}}Claude Code: run in your terminal:
Terminal window claude mcp add --transport http github-mcp http://localhost:8000/mcp -
Authenticate
Restart your coding agent to detect the server. When prompted, complete the OAuth flow. Keycard will prompt you to sign in to your zone. This is a separate account from your Keycard Console login. If it’s your first time, click Sign up to create one. You will then be prompted to authorize GitHub access.
-
Try these prompts:
- “List my GitHub repos”
- “Show open PRs on
owner/repo” - “Create an issue on
owner/repotitled ‘Bug: login broken’”
-
Verify in Audit Logs
In Keycard Console, navigate to Audit Logs to see the full flow:
Event Description users:authenticateUser logged in via Keycard users:authorizeUser authorized access to the MCP server credentials:issueAccess token issued, showing the identity chain (user + application) Each
credentials:issueevent includes an identity chain showing both the user identity (e.g.,alice@acme.com) and the application identity (e.g.,GitHub), so you can trace exactly which user triggered which API call through which application.