oauth-mcp-bridge

A generic OAuth 2.1-to-MCP authorization bridge: lets MCP clients sign in via GitHub, using standard OAuth 2.1 (RFC 8414 metadata, RFC 7591 dynamic client registration, PKCE) even though GitHub itself isn't an MCP-compliant authorization server.

Every OAuth artifact (client_id, state, authorization code, access/refresh token) is a self-owned, stateless macaroon — no database, no volume; nothing is lost across a scale-to-zero restart. Extracted from taskweft/deploy.

Modules

Wiring it up

Configure via :persistent_term before your endpoint starts (e.g. in your Application.start/2):

:persistent_term.put({:oauth_mcp_bridge, :token_secret}, token_secret)
:persistent_term.put({:oauth_mcp_bridge, :github}, %{client_id: ..., client_secret: ...})
:persistent_term.put({:oauth_mcp_bridge, :auth}, OAuthMCPBridge.Whitelist.parse("fire,@my-org"))

Then wire the endpoints into your own Plug.Router — this library provides the logic, not the route table, since which paths are public is app-specific:

alias OAuthMCPBridge.{BaseURL, Guard, LandingPlug, OAuth}
plug(:mcp_guard)
get "/" do
LandingPlug.call(conn, [])
end
get "/.well-known/oauth-protected-resource" do
send_json(conn, 200, OAuth.protected_resource_metadata(BaseURL.get(conn)))
end
get "/.well-known/oauth-authorization-server" do
send_json(conn, 200, OAuth.authorization_server_metadata(BaseURL.get(conn)))
end
post "/oauth/register" do
# decode body, call OAuth.register_client/1
end
get "/oauth/authorize" do
# call OAuth.authorize/2, redirect to the returned GitHub URL
end
get "/oauth/callback" do
# call OAuth.callback/2, redirect to the returned client URL
end
post "/oauth/token" do
# decode body, call OAuth.token/2
end
forward("/mcp", to: YourMCPPlug)
defp mcp_guard(conn, _opts) do
if public_path?(conn), do: conn, else: Guard.require_bearer(conn)
end

See taskweft/deploy's Router for a complete, working example.