# `Anubis.Server`

Build MCP servers that extend language model capabilities.

MCP servers are specialized processes that provide three core primitives to AI assistants:
**Resources** (contextual data like files or schemas), **Tools** (actions the model can invoke),
and **Prompts** (user-selectable templates). They operate in a secure, isolated architecture
where clients maintain 1:1 connections with servers, enabling composable functionality while
maintaining strict security boundaries.

## Quick Start

Create a server in three steps:

    defmodule MyServer do
      use Anubis.Server,
        name: "my-server",
        version: "1.0.0",
        capabilities: [:tools]

      component MyServer.Calculator
    end

    defmodule MyServer.Calculator do
      @moduledoc "Add two numbers"

      use Anubis.Server.Component, type: :tool

      schema do
        field :a, :number, required: true
        field :b, :number, required: true
      end

      def execute(%{a: a, b: b}, _frame) do
        {:ok, a + b}
      end
    end

    # In your supervision tree
    children = [{MyServer, transport: :stdio}]
    Supervisor.start_link(children, strategy: :one_for_one)

Your server is now a living process that AI assistants can connect to, discover available
tools, and execute calculations through a secure protocol boundary.

## Capabilities

Declare what your server can do:

- **`:tools`** - Execute functions with structured inputs and outputs
- **`:resources`** - Provide data that models can read (files, APIs, databases)
- **`:prompts`** - Offer reusable templates for common interactions
- **`:logging`** - Allow clients to configure verbosity levels

Configure capabilities with options:

    use Anubis.Server,
      capabilities: [
        :tools,
        {:resources, subscribe?: true},      # Enable resource update subscriptions
        {:prompts, list_changed?: true}      # Notify when prompts change
      ]

Extensions are declared by identifier with their settings map. They are
advertised from protocol revision 2026-07-28, which added `extensions` to
the server capabilities:

    use Anubis.Server,
      capabilities: [:tools, extensions: %{"io.modelcontextprotocol/ui" => %{}}]

## Components

Register tools, resources, and prompts as components:

    component MyServer.FileReader           # Auto-named as "file_reader"
    component MyServer.ApiClient, name: "api"   # Custom name

Components are modules that implement specific behaviors
and are automatically discovered by clients through the protocol.

## Server Lifecycle

Your server follows a predictable lifecycle with callbacks you can hook into:

1. **`init/2`** - Set up initial state when the server starts
2. **`handle_request/2`** - Process MCP protocol requests from clients
3. **`handle_notification/2`** - React to one-way client messages
4. **`handle_info/2`** - Bridge external events into MCP notifications

Most protocol handling is automatic - you typically only implement `init/2` for setup
and occasionally override other callbacks for custom behavior.

## Sending Notifications

Notification functions deliver to the Session serving the caller, so they work
from any callback: the Session's own (`init/2`, `handle_info/2`, ...) and the
request handlers of tools, prompts and resources, which run in a task the
Session starts. From any other process, use `send/2` with the session PID.

    # Inside a callback:
    def handle_info(:data_changed, frame) do
      Anubis.Server.send_tools_list_changed()
      {:noreply, frame}
    end

# `mcp_error`

```elixir
@type mcp_error() :: Anubis.MCP.Error.t()
```

# `notification`

```elixir
@type notification() :: map()
```

# `progress_step`

```elixir
@type progress_step() :: number()
```

# `progress_token`

```elixir
@type progress_token() :: String.t() | non_neg_integer()
```

# `progress_total`

```elixir
@type progress_total() :: number()
```

# `request`

```elixir
@type request() :: map()
```

# `response`

```elixir
@type response() :: map()
```

# `server_capabilities`

```elixir
@type server_capabilities() :: map()
```

# `server_info`

```elixir
@type server_info() :: map()
```

# `cache_hints`
*optional* 

```elixir
@callback cache_hints(method :: String.t()) :: %{
  ttl_ms: non_neg_integer(),
  scope: :public | :private
}
```

Returns the caching hints for a cacheable stateless-era result.

Revision 2026-07-28 requires `ttlMs` and `cacheScope` on the complete results
of `server/discover`, `tools/list`, `prompts/list`, `resources/list`,
`resources/templates/list` and `resources/read`. Optional; without it every
such result says `ttl_ms: 0` (stale at once) and `scope: :private`.

`:public` means the result is the same for every caller, so a shared cache
may serve it to anyone; answer `:private` for anything that varies with the
caller, such as a tool list filtered per user. A handler that puts `"ttlMs"`
or `"cacheScope"` in its own result keeps them. Results of a multi
round-trip retry, which carry client input, always say `ttl_ms: 0`.

## Examples

    @impl Anubis.Server
    def cache_hints("tools/list"), do: %{ttl_ms: to_timeout(minute: 5), scope: :private}
    def cache_hints(_method), do: %{ttl_ms: 0, scope: :private}

# `handle_call`
*optional* 

```elixir
@callback handle_call(
  request :: term(),
  from :: GenServer.from(),
  Anubis.Server.Frame.t()
) ::
  {:reply, reply :: term(), Anubis.Server.Frame.t()}
  | {:reply, reply :: term(), Anubis.Server.Frame.t(),
     timeout() | :hibernate | {:continue, arg :: term()}}
  | {:noreply, Anubis.Server.Frame.t()}
  | {:noreply, Anubis.Server.Frame.t(),
     timeout() | :hibernate | {:continue, arg :: term()}}
  | {:stop, reason :: term(), reply :: term(), Anubis.Server.Frame.t()}
  | {:stop, reason :: term(), Anubis.Server.Frame.t()}
```

# `handle_cast`
*optional* 

```elixir
@callback handle_cast(request :: term(), Anubis.Server.Frame.t()) ::
  {:noreply, Anubis.Server.Frame.t()}
  | {:noreply, Anubis.Server.Frame.t(),
     timeout() | :hibernate | {:continue, arg :: term()}}
  | {:stop, reason :: term(), Anubis.Server.Frame.t()}
```

# `handle_completion`
*optional* 

```elixir
@callback handle_completion(ref :: String.t(), argument :: map(), Anubis.Server.Frame.t()) ::
  {:reply, Anubis.Server.Response.t() | map(), Anubis.Server.Frame.t()}
  | {:error, mcp_error(), Anubis.Server.Frame.t()}
```

# `handle_elicitation`
*optional* 

```elixir
@callback handle_elicitation(
  response :: map(),
  request_id :: String.t(),
  Anubis.Server.Frame.t()
) ::
  {:noreply, Anubis.Server.Frame.t()}
  | {:stop, reason :: term(), Anubis.Server.Frame.t()}
```

# `handle_info`
*optional* 

```elixir
@callback handle_info(event :: term(), Anubis.Server.Frame.t()) ::
  {:noreply, Anubis.Server.Frame.t()}
  | {:noreply, Anubis.Server.Frame.t(),
     timeout() | :hibernate | {:continue, arg :: term()}}
  | {:stop, reason :: term(), Anubis.Server.Frame.t()}
```

# `handle_notification`
*optional* 

```elixir
@callback handle_notification(
  notification :: notification(),
  state :: Anubis.Server.Frame.t()
) ::
  {:noreply, new_state :: Anubis.Server.Frame.t()}
  | {:error, error :: mcp_error(), new_state :: Anubis.Server.Frame.t()}
```

Handles incoming MCP notifications from clients.

# `handle_prompt_get`
*optional* 

```elixir
@callback handle_prompt_get(
  name :: String.t(),
  arguments :: map(),
  Anubis.Server.Frame.t()
) ::
  {:reply, messages :: list(), Anubis.Server.Frame.t()}
  | {:error, mcp_error(), Anubis.Server.Frame.t()}
```

Handles a prompt get request.

# `handle_request`
*optional* 

```elixir
@callback handle_request(request :: request(), state :: Anubis.Server.Frame.t()) ::
  {:reply, response :: response(), new_state :: Anubis.Server.Frame.t()}
  | {:noreply, new_state :: Anubis.Server.Frame.t()}
  | {:error, error :: mcp_error(), new_state :: Anubis.Server.Frame.t()}
```

Low-level handler for any MCP request.

When implemented, it bypasses automatic routing to specific handlers.
Return `{:reply, response, frame}` to answer, `{:error, error, frame}` to
report a failure, or `{:noreply, frame}` to complete without a response body.
A no-reply completion is not deferred work: stateless HTTP returns 202 if
headers have not been sent, or closes an open SSE response without a final
event.

# `handle_resource_read`
*optional* 

```elixir
@callback handle_resource_read(uri :: String.t(), Anubis.Server.Frame.t()) ::
  {:reply, content :: map(), Anubis.Server.Frame.t()}
  | {:error, mcp_error(), Anubis.Server.Frame.t()}
```

Handles a resource read request.

# `handle_roots`
*optional* 

```elixir
@callback handle_roots(
  roots :: [map()],
  request_id :: String.t(),
  Anubis.Server.Frame.t()
) ::
  {:noreply, Anubis.Server.Frame.t()}
  | {:stop, reason :: term(), Anubis.Server.Frame.t()}
```

# `handle_sampling`
*optional* 

```elixir
@callback handle_sampling(
  response :: map(),
  request_id :: String.t(),
  Anubis.Server.Frame.t()
) ::
  {:noreply, Anubis.Server.Frame.t()}
  | {:stop, reason :: term(), Anubis.Server.Frame.t()}
```

# `handle_session_expired`
*optional* 

```elixir
@callback handle_session_expired(session_id :: String.t(), Anubis.Server.Frame.t()) ::
  {:ok, Anubis.Server.Frame.t()}
  | {:ok, client_info :: map(), Anubis.Server.Frame.t()}
  | {:error, reason :: term()}
```

Called when a session is being auto-recovered after expiry.

Invoked during `auto_initialize/1` instead of the normal client handshake.
Receives the session ID and the current frame (pre-populated from the session
store if one is configured).

Return values:
- `{:ok, frame}` — accept recovery using synthetic client info
- `{:ok, client_info, frame}` — accept recovery and supply real client info
- `{:error, reason}` — reject recovery; the client receives an internal error

If this callback is not implemented, the default behavior is unchanged:
synthetic client info is used and `init/2` is called normally.

# `handle_tool_call`
*optional* 

```elixir
@callback handle_tool_call(
  name :: String.t(),
  arguments :: map(),
  Anubis.Server.Frame.t()
) ::
  {:reply, result :: term(), Anubis.Server.Frame.t()}
  | {:error, mcp_error(), Anubis.Server.Frame.t()}
```

Handles a tool call request.

This callback is invoked when a client calls a specific tool. It receives the tool name,
the arguments provided by the client, and the current frame.

# `init`
*optional* 

```elixir
@callback init(client_info :: map(), Anubis.Server.Frame.t()) ::
  {:ok, Anubis.Server.Frame.t()} | {:error, term()}
```

Called after a client requests a `initialize` request.

This callback is invoked while the MCP handshake starts and so the client may not sent
the `notifications/initialized` message yet. For checking if the notification was already sent
and the MCP handshake was successfully completed, you can check the `context.initialized` field
in the frame.

It receives the client's information and
the current frame, allowing you to perform client-specific setup, validate capabilities,
or prepare resources based on the connected client.

Returning `{:error, reason}` rejects the session: the transport answers the
request with an encodable JSON-RPC error. Return an `Anubis.MCP.Error` struct
to control the error code sent to the client; any other term is wrapped as an
internal error with a stringified message.

Requests of the stateless era (2026-07-28 onward) have no handshake. Over
Streamable HTTP each one is served by a session of its own, and `init/2` runs
for every such request, with the client info that request carries, before
it is handled; `clientInfo` is optional in that era, and a request without
it passes an empty map. The frame it returns lives only as long as the request.

# `serialize_assigns`
*optional* 

```elixir
@callback serialize_assigns(assigns :: map()) :: map()
```

Transforms `frame.assigns` into a JSON-safe map before session persistence.

`frame.assigns` is copied verbatim into the session store. Runtime values that
are not JSON-encodable (Ecto structs, `MapSet`, PIDs, functions, etc.) make
persistence fail. Implement this callback to project assigns down to the
JSON-safe subset you actually need persisted (e.g. `%{"user_id" => id}`),
avoiding storing full domain structs or PII.

On recovery, the restored frame is handed back to `handle_session_expired/2`,
where the host rehydrates runtime state from these persisted values.

Defaults to the identity function when not implemented.

# `server_capabilities`

```elixir
@callback server_capabilities() :: server_capabilities()
```

# `server_info`

```elixir
@callback server_info() :: server_info()
```

# `server_instructions`
*optional* 

```elixir
@callback server_instructions() :: String.t() | nil
```

Returns optional instructions describing how to use the server and its features.

This can be used by clients to improve the LLM's understanding of available tools,
resources, etc. It can be thought of like a "hint" to the model. For example, this
information MAY be added to the system prompt.

Return `nil` to omit the instructions field from the initialize response.

# `server_instructions`
*optional* 

```elixir
@callback server_instructions(frame :: Anubis.Server.Frame.t()) :: String.t() | nil
```

Returns the instructions for *this* connection, given the frame the handshake will answer with.

Optional, and takes precedence over `c:server_instructions/0` when defined. The frame carries
the transport's assigns — whatever a `Plug` put on the connection before the request reached
the server — so a server whose guidance depends on who is asking can vary it here.

`c:init/2` cannot serve this purpose: it runs when the client acknowledges the handshake,
after the `initialize` result carrying the instructions has already been sent.

## Examples

    @impl Anubis.Server
    def server_instructions(frame) do
      case frame.assigns[:plan] do
        :enterprise -> "You may call `bulk_export`. Confirm the range before exporting."
        _ -> "Exports run one record at a time."
      end
    end

Return `nil` to omit the field from the `initialize` result.

# `server_tools`
*optional* 

```elixir
@callback server_tools(
  tools :: [Anubis.Server.Component.Tool.t()],
  frame :: Anubis.Server.Frame.t()
) :: [
  Anubis.Server.Component.Tool.t()
]
```

Returns the tools *this* connection may list and call, given its frame.

Optional. It is applied to the compile-time components concatenated with the frame's runtime
tools, every time the list is built — which is both `tools/list` and the lookup `tools/call`
does, so a tool withheld here cannot be reached by name either.

The frame carries the transport's assigns, so a server whose surface depends on who is asking
resolves it here rather than at registration. `c:init/2` cannot serve this purpose: it runs
once per session, and on a session restored from a store it runs before the request's assigns
are attached.

Return the tools unchanged to serve everyone the same surface. A tool may also be returned
*rewritten* — a description or a schema that names something one caller may not be shown is a
connection-level decision, and the struct is the place to make it.

## Examples

    @impl Anubis.Server
    def server_tools(tools, frame) do
      if frame.assigns[:plan] == :enterprise do
        tools
      else
        Enum.reject(tools, &(&1.name == "bulk_export"))
      end
    end

# `supported_protocol_versions`

```elixir
@callback supported_protocol_versions() :: [String.t()]
```

Returns the protocol versions served by this server.

Defaults to all registered versions, including the stateless era. Set the
`:protocol_versions` use option or implement this callback to restrict them.
The `initialize` handshake only negotiates legacy versions.

# `terminate`
*optional* 

```elixir
@callback terminate(reason :: term(), Anubis.Server.Frame.t()) :: term()
```

# `component`
*macro* 

Registers a component (tool, prompt, or resource) with the server.

# `notify_elicitation_complete`

```elixir
@spec notify_elicitation_complete(String.t()) :: :ok
```

Sends a `notifications/elicitation/complete` notification.

Tells the client that the out-of-band interaction started by a URL-mode
elicitation finished. Delivery is not guaranteed, so clients must not block
on it, but it lets them react programmatically.

# `send_elicitation_request`

```elixir
@spec send_elicitation_request(String.t(), map(), configuration) ::
  :ok | {:error, term()}
when configuration: [{:timeout, non_neg_integer() | nil}]
```

Sends an `elicitation/create` request to the client.

Per the MCP 2025-06-18 specification, the server provides a human-readable
`message` and a restricted-subset JSON `requested_schema` describing the
expected user input. The client presents this to the user and returns one of
three actions: `accept` (with content matching the schema), `decline`, or
`cancel`.

This is an asynchronous operation. The response will be delivered to your
`handle_elicitation/3` callback.

The `requested_schema` is validated synchronously before any wire I/O. The
client must advertise the `elicitation` capability or the call returns
`{:error, :capability_not_supported}` after enqueueing.

## Schema Subset

  * Top level must be `%{"type" => "object", "properties" => %{...}}`
  * Properties may declare `"type"` of `"string"`, `"number"`, `"integer"`,
    `"boolean"`, or use `"enum"` (string-only)
  * String properties may set `"format"` of `"email"`, `"uri"`, `"date"`,
    or `"date-time"`

Per the spec, **servers MUST NOT request sensitive information** through
elicitation.

## Example

    defmodule MyServer.Tools.Greet do
      use Anubis.Server.Component, type: :tool

      @impl true
      def execute(_args, frame) do
        Anubis.Server.send_elicitation_request("What's your name?", %{
          "type" => "object",
          "properties" => %{
            "name" => %{"type" => "string", "minLength" => 1}
          },
          "required" => ["name"]
        })

        {:reply, "asked for name", frame}
      end
    end

# `send_log_message`

```elixir
@spec send_log_message(
  level :: Logger.level(),
  message :: String.t(),
  metadata :: term()
) :: :ok
```

Sends a log message to the client, as a `notifications/message`.

The notification carries `level` and `data`. `data` is `message` alone, or
`%{"message" => message, "data" => data}` when `data` is given. Messages below
the client's log threshold are dropped. Stateless requests receive logs only
when they declare `io.modelcontextprotocol/logLevel`; HTTP also requires SSE.
Legacy sessions without a configured threshold receive all levels.

Returns `:ok` after enqueueing; this does not acknowledge client delivery.

**Must be called from a callback** — see `send_resources_list_changed/0` for details.

# `send_progress`

```elixir
@spec send_progress(progress_token(), progress_step(), opts) :: :ok
when opts: [total: progress_total(), message: String.t()]
```

Sends a progress notification for an ongoing operation.

Pass the client's token from `Anubis.Server.Frame.progress_token/1`, the
current progress value, and optional `:total` and `:message` fields. Call
only when the request supplies a token. The notification is queued for the
session serving the callback; `:ok` does not acknowledge client delivery.
Stateless HTTP delivers progress on the request's response when it accepts SSE.

# `send_prompts_list_changed`

```elixir
@spec send_prompts_list_changed() :: :ok
```

Sends a prompts list changed notification.

**Must be called from a callback** — see `send_resources_list_changed/0` for details.

# `send_resource_updated`

```elixir
@spec send_resource_updated(uri :: String.t(), timestamp :: DateTime.t() | nil) :: :ok
```

Sends a resource updated notification for a specific resource.

Subscription-gated: only emits if the current session has previously
received a `resources/subscribe` request for this URI. Calls for
unsubscribed URIs are silently dropped.

**Must be called from a callback** — see `send_resources_list_changed/0` for details.

# `send_resources_list_changed`

```elixir
@spec send_resources_list_changed() :: :ok
```

Sends a resources list changed notification.

**Must be called from a callback** — a Session callback or a tool, prompt or
resource handler. From any other process the message is lost.

For external processes, use `send(session_pid, {:send_notification, "notifications/resources/list_changed", %{}})`.

# `send_roots_request`

```elixir
@spec send_roots_request([{:timeout, non_neg_integer() | nil}]) :: :ok
```

Sends a roots/list request to the client.

# `send_sampling_request`

```elixir
@spec send_sampling_request([map()], configuration) :: :ok
when configuration: [
       model_preferences: map() | nil,
       system_prompt: String.t() | nil,
       max_tokens: non_neg_integer() | nil,
       timeout: non_neg_integer() | nil
     ]
```

Sends a sampling/createMessage request to the client.

This is an asynchronous operation. The response will be delivered to your
`handle_sampling/3` callback.

# `send_task_status`

```elixir
@spec send_task_status(task_id :: String.t()) :: :ok
```

Sends a `notifications/tasks/status` notification with the current state of
the given task.

**Must be called from a callback** — see
`send_resources_list_changed/0` for details.

Per spec (2025-11-25), receivers MAY send these notifications when a task's
status changes; they are optional and requestors MUST NOT rely on them. This
helper looks up the task in the configured task store and emits the full
`Task` projection.

# `send_tools_list_changed`

```elixir
@spec send_tools_list_changed() :: :ok
```

Sends a tools list changed notification.

**Must be called from a callback** — see `send_resources_list_changed/0` for details.

# `send_url_elicitation_request`

```elixir
@spec send_url_elicitation_request(String.t(), String.t(), String.t(), configuration) ::
  :ok | {:error, term()}
when configuration: [{:timeout, non_neg_integer() | nil}]
```

Sends a URL-mode `elicitation/create` request to the client.

URL mode is the out-of-band form of elicitation: the user completes the
interaction in their browser and the client never sees the sensitive data.
That makes it the right mode for credentials, third-party OAuth and payment
flows, all of which in-band form elicitation must never carry.

Unlike the form-mode call, there is no `requested_schema`: the parameters are
`url`, the opaque `elicitation_id`, and `message`. The client answers with an
action and no content.

The client must declare the `url` elicitation mode
(`%{"elicitation" => %{"url" => %{}}}`). A client that declares only
`elicitation: %{}` is form-only by specification, so the request is dropped
as a missing-capability failure.

After the interaction finishes, send `notify_elicitation_complete/1`.

## Example

    Anubis.Server.send_url_elicitation_request(
      "https://github.com/login/oauth/authorize?client_id=abc",
      "550e8400-e29b-41d4-a716-446655440000",
      "Authorize access to your GitHub repositories"
    )

---

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