Packages

fnord

0.9.39

AI code archaeology

Current section

Files

Jump to
fnord lib mcp oauth2 registration.ex
Raw

lib/mcp/oauth2/registration.ex

defmodule MCP.OAuth2.Registration do
@moduledoc """
RFC 7591 Dynamic Client Registration for OAuth2.
Allows automatic registration of native clients with OAuth providers.
"""
@doc """
Register a new OAuth client with the authorization server.
Uses RFC 7591 Dynamic Client Registration to automatically obtain a client_id
without requiring manual pre-registration.
## Parameters
- registration_endpoint: The registration endpoint URL from discovery
- opts: Optional overrides
- :client_name - Application name (default: "fnord")
- :redirect_uris - Callback URIs (default: ["http://localhost:8080/callback"])
## Returns
- `{:ok, %{client_id: String.t(), client_secret: String.t() | nil}}`
- `{:error, reason}`
"""
@spec register(String.t(), keyword()) ::
{:ok, %{client_id: String.t(), client_secret: String.t() | nil}} | {:error, term()}
def register(registration_endpoint, opts \\ []) do
# RFC 8252 section 7.3: Native apps should use loopback with ephemeral port
# Since many servers require exact URI matches, we use a fixed port (8080)
default_redirects = [
"http://localhost:8080/callback"
]
request_body = %{
"client_name" => opts[:client_name] || "fnord",
"redirect_uris" => opts[:redirect_uris] || default_redirects,
"grant_types" => ["authorization_code", "refresh_token"],
"response_types" => ["code"],
"token_endpoint_auth_method" => "none",
"application_type" => "native"
}
headers = [{"Content-Type", "application/json"}]
case Http.Client.impl().post(
registration_endpoint,
SafeJson.encode!(request_body),
headers,
recv_timeout: 15_000,
timeout: 15_000
) do
{:ok, %{status_code: 201, body: body}} ->
parse_registration_response(body)
{:ok, %{status_code: 200, body: body}} ->
# Some servers return 200 instead of 201
parse_registration_response(body)
{:ok, %{status_code: code, body: _body}} ->
{:error, {:registration_failed, code}}
end
rescue
e in HTTPoison.Error ->
{:error, {:network_error, e.reason}}
catch
_kind, reason ->
{:error, {:network_error, reason}}
end
defp parse_registration_response(body) do
case SafeJson.decode(body) do
{:ok, %{"client_id" => client_id} = response} ->
result = %{
client_id: client_id,
client_secret: Map.get(response, "client_secret")
}
MCP.Util.debug("MCP OAuth", "Successfully registered client: #{client_id}")
{:ok, result}
{:ok, _} ->
{:error, :missing_client_id}
{:error, {:invalid_json, reason}} ->
{:error, {:invalid_json, reason}}
end
end
end