# `PhoenixKit.API.PublicDiscovery`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.64.1/lib/phoenix_kit/api/public_discovery.ex#L1)

What the install publishes, without a key, about its public API: the guide
`GET /mcp`, the API catalog `/.well-known/api-catalog` (RFC 9727), the `Link`
headers that point to them and the API section of `/llms.txt`.

Everything here is built from `PhoenixKit.API.Registry.visible_tools/2` of
the **public** audience on every request (nothing is cached), so a module
that is switched off or a tool hidden in Settings -> API is gone from the
documents at once. Nothing here names the admin API: a document served
without a key never mentions an admin path, tool, scope or permission.

## One predicate

`check/1` and `visible?/1` decide whether anything is published at all, and
every document and header of this module asks it:

  1. the public API switch is on (`PhoenixKit.API.Config.public_enabled?/0`);
  2. the website password gate does not hide the site from this client
     (`PhoenixKit.API.Auth.gated?/2`): a gated pre-production site publishes
     nothing it would not serve through the API itself;
  3. at least one public tool is visible. The list is returned, so one
     request walks the registry once.

## Which host a document names

The `Host` header is never trusted by itself (`PhoenixKit.API.RequestContext`
defaults its `:hosts` to the request host, so "the host is one of the hosts"
proves nothing on a default install). `origin/1` uses the request host only
when it is a well-formed hostname **and** one of the hosts
`PhoenixKit.API.MCP.Origin.known_hosts/1` lists (the canonical host and the
hosts the `:api_request_context` hook explicitly returns); else it uses
`PhoenixKit.API.MCP.Origin.canonical_origin/0`. Never
`PhoenixKit.Utils.Routes.base_url/0`, which reads the database uncached.

The scheme and the port never come from the request either, whatever its
`Host` port or `X-Forwarded-Proto` say: the canonical host is named by
`canonical_origin/0` as it is, any other known host takes the canonical
scheme and the canonical port (none when that is the scheme's default).
The documents are cached publicly, so a request must not shape them.

The API and the guide live under the kit's URL prefix (`PhoenixKit.Utils.Routes.path/1`);
`/llms.txt` and `/.well-known/*` are origin-root URLs.

# `api_catalog`

```elixir
@spec api_catalog(String.t()) :: map()
```

The API catalog (RFC 9727 linkset) naming the origin's public API.

# `api_url`

```elixir
@spec api_url(String.t()) :: String.t()
```

The URL of the public REST API under `origin`.

# `cache_control`

```elixir
@spec cache_control() :: String.t()
```

The `Cache-Control` of a published document.

While the website gate is on the answer depends on the client address, so a
shared cache must not keep it.

# `catalog_content_type`

```elixir
@spec catalog_content_type() :: String.t()
```

The `Content-Type` of the API catalog.

# `catalog_link`

```elixir
@spec catalog_link(String.t()) :: String.t()
```

The `Link` header value that points to the API catalog.

# `catalog_url`

```elixir
@spec catalog_url(String.t()) :: String.t()
```

The URL of the API catalog: at the origin root, whatever the URL prefix.

# `check`

```elixir
@spec check(Plug.Conn.t()) :: {:ok, [PhoenixKit.API.Tool.t()]} | :hidden
```

Whether anything is published for `conn`, with the public tools that make it
so. The three parts are checked in the order of the moduledoc, cheapest first.

# `guide`

```elixir
@spec guide(Plug.Conn.t(), [PhoenixKit.API.Tool.t()]) :: map()
```

The content of the guide for `conn`, as one map the two renderers
(`PhoenixKit.API.PublicDiscovery.GuideHTML`, `PhoenixKit.API.PublicDiscovery.GuideMarkdown`)
turn into text. `tools` is the list `check/1` returned.

# `guide_url`

```elixir
@spec guide_url(String.t()) :: String.t()
```

The URL of the guide under `origin`.

# `language`

```elixir
@spec language(PhoenixKit.API.RequestContext.t()) :: String.t()
```

The language a page for `context` is written in: the domain's language
reduced to a locale `PhoenixKitWeb.Gettext` has a catalogue for, else `"en"`.

# `llms_section`

```elixir
@spec llms_section(String.t()) :: String.t()
```

The API section of `/llms.txt` for `origin`, to append after the operator's text.

# `llms_url`

```elixir
@spec llms_url(String.t()) :: String.t()
```

The URL of `/llms.txt`: at the origin root, whatever the URL prefix.

# `mcp_url`

```elixir
@spec mcp_url(String.t()) :: String.t()
```

The URL of the public MCP server under `origin`.

# `openapi_url`

```elixir
@spec openapi_url(String.t()) :: String.t()
```

The URL of the public OpenAPI description under `origin`.

# `origin`

```elixir
@spec origin(Plug.Conn.t()) :: String.t()
```

The origin (`scheme://host[:port]`) the documents for `conn` name, by the
rule of the moduledoc.

# `put_link`

```elixir
@spec put_link(Plug.Conn.t(), String.t()) :: Plug.Conn.t()
```

Adds `value` to the `Link` header of `conn`, after the value an earlier plug
may have set (one header, values joined by a comma, as RFC 8288 allows). A
value that is already there is not repeated, and a blank earlier header is
ignored.

# `put_list_links`

```elixir
@spec put_list_links(Plug.Conn.t()) :: Plug.Conn.t()
```

The `Link` header of `GET /api/public/v1`, the tool list: the OpenAPI
description and the guide. Added on a `200` only, and only when the guide
is published; the check runs when the answer is sent.

# `put_service_doc_link`

```elixir
@spec put_service_doc_link(Plug.Conn.t()) :: Plug.Conn.t()
```

Adds `Link: <guide>; rel="service-doc"` to the `405` of `GET /mcp/public` when
the guide is published.

# `visible?`

```elixir
@spec visible?(Plug.Conn.t()) :: boolean()
```

Whether anything is published for `conn` (`check/1`).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
