The last step: you don't host the server at all. GitHub hosts its MCP server
remotely at api.githubcopilot.com/mcp/, behind OAuth. ToolHive runs an
MCPRemoteProxy with an embedded OAuth auth server in the cluster: it does the
GitHub OAuth upstream and lets the agent authenticate to the proxy. The agent
loop is unchanged from Part 3 — main.py just adds an OAuth handshake.
This is the advanced setup. Don't sweat the cluster internals — the auth is brokered on the ToolHive side; you run two steps, approve once in the browser, and the agent calls GitHub's real tools.
- The MCP server is remote and GitHub's — no container of ours.
- It's behind OAuth. ToolHive's embedded auth server brokers it: the agent does an authorization-code + PKCE flow against the proxy, which chains to GitHub; the human approves once.
main.pygains anOAuthClientProvider(folded inline — still the only Python file).list_tools/call_tooland the agent loop are otherwise identical.
main.py— given. Discovers GitHub's remote tools, runs the shared async loop, with the OAuth handshake inline (no separate client file).toolhive/github-remote.yaml— the three resources deployed to the cluster:MCPOIDCConfig+MCPExternalAuthConfig(embedded auth server) +MCPRemoteProxy.cluster-prereqs.sh— Step 1: cluster + operator + the auth server's secrets.
Create a GitHub OAuth app (github.com/settings/developers):
- Authorization callback URL:
http://localhost:8080/oauth/callback - Put its client id in
toolhive/github-remote.yaml(clientId) — the workshop app id is already filled in. - Put its client secret in
.envasGITHUB_OAUTH_CLIENT_SECRET=…(Part 4 reads.envonly — it never looks at your shell environment).
(The agent's own callback — localhost:8765 — is registered dynamically with
ToolHive's auth server, not with GitHub, so it needs no app config.)
Two steps. The first is a script; the second you type by hand — deploying the proxy + running the agent is the Part-4 moment.
# Step 1/2 — cluster + operator + the embedded auth server's secrets.
# Reads GITHUB_OAUTH_CLIENT_SECRET from .env (not your shell env);
# deletes + recreates all three secrets each run. (Prints Step 2.)
bash parts/04_remote_oauth/cluster-prereqs.sh
# Step 2/2 — deploy the proxy + auth server, expose it, run the agent (type these):
kubectl apply -f parts/04_remote_oauth/toolhive/github-remote.yaml
kubectl wait --for=condition=Ready mcpremoteproxy/github-remote \
-n toolhive-system --timeout=120s
kubectl port-forward -n toolhive-system svc/mcp-github-remote-remote-proxy 8080:8080 &
uv run python parts/04_remote_oauth/main.py # opens a browser to approve, then runsThe proxy speaks MCP over HTTP behind OAuth, and Claude Code is an OAuth-capable
MCP client — so it can use the remote server too, doing the same browser
handshake main.py does. With the port-forward from Step 2 still running:
claude mcp add --transport http github-remote http://localhost:8080/mcpThen trigger the OAuth flow from inside Claude Code: run /mcp, pick
github-remote, and authenticate. A browser opens to approve (the embedded auth
server chains through to GitHub); after that Claude Code calls GitHub's real
remote tools through the proxy. Remove it with claude mcp remove github-remote.
Use localhost, not 127.0.0.1: the auth server's issuer and audience are
pinned to http://localhost:8080/ (see github-remote.yaml), and the OAuth
resource indicator must match that host exactly or the handshake fails.
Auth is the most failure-prone live step (browser redirects, issuer config, token refresh). Keeping it last means the rest of the tutorial runs even if this is skipped.
bash cluster/uninstall.sh # deletes the kind cluster