Current section

Files

Jump to
codex_sdk lib codex app_server mcp.ex
Raw

lib/codex/app_server/mcp.ex

defmodule Codex.AppServer.Mcp do
@moduledoc """
MCP (Model Context Protocol) server management for app-server connections.
This module provides functions to interact with MCP servers configured in
the Codex app-server, including listing server status and handling OAuth
authentication flows.
"""
alias Codex.AppServer.Connection
alias Codex.AppServer.Params
alias Codex.Config.Defaults
alias Codex.MCP.Config, as: MCPConfig
alias Codex.MCP.OAuth
@type connection :: pid()
@doc """
Lists configured MCP servers with their tools, resources, and auth status.
Supports cursor-based pagination via `:cursor` and `:limit` options.
## Compatibility
This function tries the new `mcpServerStatus/list` method first. If the server
returns a "method not found" (`-32601`) or "unknown variant" (`-32600`) error
(older servers), it falls back to the legacy `mcpServers/list` method
automatically.
"""
@spec list_servers(connection(), keyword()) :: {:ok, map()} | {:error, term()}
def list_servers(conn, opts \\ []) when is_pid(conn) and is_list(opts) do
params =
%{}
|> Params.put_optional("cursor", Keyword.get(opts, :cursor))
|> Params.put_optional("limit", Keyword.get(opts, :limit))
case Connection.request(conn, "mcpServerStatus/list", params,
timeout_ms: Defaults.mcp_server_request_timeout_ms()
) do
{:error, %{"code" => -32_601}} ->
Connection.request(conn, "mcpServers/list", params,
timeout_ms: Defaults.mcp_server_request_timeout_ms()
)
{:error, %{code: -32_601}} ->
Connection.request(conn, "mcpServers/list", params,
timeout_ms: Defaults.mcp_server_request_timeout_ms()
)
{:error, error} ->
if unknown_variant_mcp_server_status_list?(error) do
Connection.request(conn, "mcpServers/list", params,
timeout_ms: Defaults.mcp_server_request_timeout_ms()
)
else
{:error, error}
end
result ->
result
end
end
@doc """
Alias for `list_servers/2`. Returns MCP server status information.
"""
@spec list_server_statuses(connection(), keyword()) :: {:ok, map()} | {:error, term()}
def list_server_statuses(conn, opts \\ []), do: list_servers(conn, opts)
@doc """
Starts an OAuth login flow for a streamable HTTP MCP server.
OAuth credentials are stored using the configured MCP credentials store. Use
`oauth_tokens/3` to load them after login completes.
"""
@spec oauth_login(connection(), keyword()) :: {:ok, map()} | {:error, term()}
def oauth_login(conn, opts) when is_pid(conn) and is_list(opts) do
name = Keyword.fetch!(opts, :name)
params =
%{"name" => name}
|> Params.put_optional("scopes", Keyword.get(opts, :scopes))
|> Params.put_optional("timeoutSecs", Keyword.get(opts, :timeout_secs))
Connection.request(conn, "mcpServer/oauth/login", params,
timeout_ms: Defaults.mcp_server_request_timeout_ms()
)
end
@doc """
Loads stored OAuth tokens for a configured MCP server.
Returns `{:error, :server_not_found}` if the server is not configured, or
`{:error, :missing_url}` if the server is not a streamable HTTP server.
"""
@spec oauth_tokens(connection(), String.t(), keyword()) ::
{:ok, OAuth.tokens() | nil} | {:error, term()}
def oauth_tokens(conn, name, opts \\ [])
when is_pid(conn) and is_binary(name) and is_list(opts) do
store_mode = Keyword.get(opts, :store_mode)
with {:ok, servers} <- MCPConfig.list_servers(conn, opts),
{:ok, server} <- Map.fetch(servers, name),
url when is_binary(url) <- fetch_server_url(server) do
{:ok, OAuth.load_tokens(name, url, store_mode)}
else
:error -> {:error, :server_not_found}
false -> {:error, :missing_url}
nil -> {:error, :missing_url}
{:error, _} = error -> error
end
end
@doc """
Deletes stored OAuth tokens for a configured MCP server.
Returns `{:error, :server_not_found}` if the server is not configured, or
`{:error, :missing_url}` if the server is not a streamable HTTP server.
"""
@spec oauth_logout(connection(), String.t(), keyword()) :: :ok | {:error, term()}
def oauth_logout(conn, name, opts \\ [])
when is_pid(conn) and is_binary(name) and is_list(opts) do
store_mode = Keyword.get(opts, :store_mode)
with {:ok, servers} <- MCPConfig.list_servers(conn, opts),
{:ok, server} <- Map.fetch(servers, name),
url when is_binary(url) <- fetch_server_url(server) do
OAuth.delete_tokens(name, url, store_mode)
else
:error -> {:error, :server_not_found}
false -> {:error, :missing_url}
nil -> {:error, :missing_url}
{:error, _} = error -> error
end
end
@doc """
Requests MCP servers to reload configuration and refresh cached tools.
"""
@spec reload(connection()) :: {:ok, map()} | {:error, term()}
def reload(conn) when is_pid(conn) do
Connection.request(conn, "config/mcpServer/reload", %{},
timeout_ms: Defaults.mcp_server_request_timeout_ms()
)
end
defp unknown_variant_mcp_server_status_list?(%{"code" => -32_600, "message" => message})
when is_binary(message) do
String.contains?(message, "unknown variant") and
String.contains?(message, "mcpServerStatus/list")
end
defp unknown_variant_mcp_server_status_list?(%{code: -32_600, message: message})
when is_binary(message) do
String.contains?(message, "unknown variant") and
String.contains?(message, "mcpServerStatus/list")
end
defp unknown_variant_mcp_server_status_list?(_error), do: false
defp fetch_server_url(%{} = server) do
Map.get(server, "url") || Map.get(server, :url)
end
end