# `Anubis.Server.Frame`

The Anubis Frame.

This module defines a struct and functions for working with
MCP server state throughout the request/response lifecycle.

## User fields

These fields contain user-controlled data:

  * `assigns` - shared user data as a map. For HTTP transports, this inherits
    from `Plug.Conn.assigns`. Users are responsible for populating authentication
    data through their Plug pipeline before it reaches the MCP server.

## Transport fields

These fields contain transport-specific context. The structure varies by transport type:

### HTTP transport (when `transport.type == :http`)

  * `req_headers` - the request headers as a list, example: `[{"content-type", "application/json"}]`.
    All header names are downcased.
  * `query_params` - the request query params as a map, example: `%{"session" => "abc123"}`.
    Returns `nil` if query params were not fetched by the Plug pipeline.
  * `remote_ip` - the IP of the client, example: `{151, 236, 219, 228}`.
    This field is set by the transport layer.
  * `scheme` - the request scheme as an atom, example: `:https`
  * `host` - the requested host as a binary, example: `"api.example.com"`
  * `port` - the requested port as an integer, example: `443`
  * `request_path` - the requested path, example: `"/mcp"`

### STDIO transport (when `transport.type == :stdio`)

  * `env` - environment variables as a map, example: `%{"USER" => "alice", "HOME" => "/home/alice"}`
  * `pid` - the OS process ID as a string, example: `"12345"`

## MCP protocol fields

These fields contain MCP-specific data:

  * `request` - the current MCP request being processed, with fields:
    * `id` - the request ID for correlation
    * `method` - the MCP method being called, example: `"tools/call"`
    * `params` - the raw request parameters (before validation)
  * `initialized` - boolean indicating if the MCP session has been initialized

## Private fields

These fields are reserved for framework usage:

  * `private` - shared framework data as a map. Contains MCP session context:
    * `session_id` - unique identifier for the current client session being handled
    * `client_info` - client information from initialization, example: `%{"name" => "my-client", "version" => "1.0.0"}`
    * `client_capabilities` - negotiated client capabilities
    * `protocol_version` - active MCP protocol version, example: `"2025-03-26"`

# `http_t`

```elixir
@type http_t() :: %{
  type: :http,
  req_headers: [{String.t(), String.t()}],
  query_params: %{optional(String.t()) =&gt; String.t()} | nil,
  remote_ip: term(),
  scheme: :http | :https,
  host: String.t(),
  port: non_neg_integer(),
  request_path: String.t()
}
```

# `private_t`

```elixir
@type private_t() :: %{
  optional(:session_id) =&gt; String.t(),
  optional(:client_info) =&gt; map(),
  optional(:client_capabilities) =&gt; map(),
  optional(:protocol_version) =&gt; String.t(),
  optional(:server_module) =&gt; module(),
  optional(:server_registry) =&gt; module(),
  optional(:pagination_limit) =&gt; non_neg_integer(),
  optional(:__mcp_components__) =&gt; [server_component_t()]
}
```

# `request_t`

```elixir
@type request_t() :: %{id: String.t(), method: String.t(), params: map()}
```

# `server_component_t`

```elixir
@type server_component_t() ::
  Anubis.Server.Component.Tool.t()
  | Anubis.Server.Component.Resource.t()
  | Anubis.Server.Component.Prompt.t()
```

# `stdio_t`

```elixir
@type stdio_t() :: %{type: :stdio, os_pid: non_neg_integer(), env: map()}
```

# `t`

```elixir
@type t() :: %Anubis.Server.Frame{
  assigns: Enumerable.t(),
  initialized: boolean(),
  private: private_t(),
  request: request_t() | nil,
  transport: transport_t()
}
```

# `transport_t`

```elixir
@type transport_t() :: http_t() | stdio_t()
```

# `assign`

```elixir
@spec assign(t(), Enumerable.t()) :: t()
```

Assigns a value or multiple values to the frame.

## Examples

    # Single assignment
    frame = Frame.assign(frame, :status, :active)

    # Multiple assignments via map
    frame = Frame.assign(frame, %{status: :active, count: 5})

    # Multiple assignments via keyword list
    frame = Frame.assign(frame, status: :active, count: 5)

# `assign`

```elixir
@spec assign(t(), key :: atom(), value :: any()) :: t()
```

# `assign_new`

```elixir
@spec assign_new(t(), key :: atom(), value_fun :: (-&gt; term())) :: t()
```

Assigns a value to the frame only if the key doesn't already exist.

The value is computed lazily using the provided function, which is only
called if the key is not present in assigns.

## Examples

    # Only assigns if :timestamp doesn't exist
    frame = Frame.assign_new(frame, :timestamp, fn -> DateTime.utc_now() end)

    # Function is not called if key exists
    frame = frame |> Frame.assign(:count, 5)
                  |> Frame.assign_new(:count, fn -> expensive_computation() end)
    # count remains 5

# `clear_components`

```elixir
@spec clear_components(t()) :: t()
```

Clears all current registered components (tools, resources, prompts)

# `clear_request`

```elixir
@spec clear_request(t()) :: t()
```

Clears the current request from the frame.

This should be called after processing a request to ensure the frame doesn't
retain stale request data.

## Examples

    frame = Frame.clear_request(frame)

# `clear_session`

```elixir
@spec clear_session(t()) :: t()
```

Clears all session-specific private data from the frame.

This should be called when a session ends to ensure the frame doesn't
retain stale session data.

## Examples

    frame = Frame.clear_session(frame)

# `get_client_capabilities`

```elixir
@spec get_client_capabilities(t()) :: map() | nil
```

Gets the client capabilities from the frame's private data.

## Examples

    capabilities = Frame.get_client_capabilities(frame)
    # => %{"tools" => %{}, "resources" => %{}}

# `get_client_info`

```elixir
@spec get_client_info(t()) :: map() | nil
```

Gets the client info from the frame's private data.

## Examples

    client_info = Frame.get_client_info(frame)
    # => %{"name" => "my-client", "version" => "1.0.0"}

# `get_components`

```elixir
@spec get_components(t()) :: [server_component_t()]
```

Retrieves all current registered components (tools, resources, prompts)

# `get_mcp_session_id`

```elixir
@spec get_mcp_session_id(t()) :: String.t() | nil
```

Gets the MCP session ID from the frame's private data.

## Examples

    session_id = Frame.get_mcp_session_id(frame)
    # => "session_abc123"

# `get_protocol_module`

```elixir
@spec get_protocol_module(t()) :: module() | nil
```

Gets the negotiated protocol module from the frame's private data.

Returns the module implementing `Anubis.Protocol.Behaviour` for the
negotiated protocol version, or nil if not yet negotiated.

## Examples

    mod = Frame.get_protocol_module(frame)
    # => Anubis.Protocol.V2025_03_26

# `get_protocol_version`

```elixir
@spec get_protocol_version(t()) :: String.t() | nil
```

Gets the protocol version from the frame's private data.

## Examples

    version = Frame.get_protocol_version(frame)
    # => "2025-03-26"

# `get_query_param`

```elixir
@spec get_query_param(t(), String.t()) :: String.t() | nil
```

Gets a query parameter value from HTTP transport.

Returns the parameter value, or nil if the transport is not HTTP,
query params weren't fetched, or the parameter doesn't exist.

## Examples

    # HTTP transport with query params
    session = Frame.get_query_param(frame, "session")
    # => "abc123"

    # Missing parameter or non-HTTP transport
    missing = Frame.get_query_param(frame, "nonexistent")
    # => nil

# `get_req_header`

```elixir
@spec get_req_header(t(), String.t()) :: String.t() | nil
```

Gets a request header value from HTTP transport.

Returns the first value for the header, or nil if the transport
is not HTTP or the header is not present.

## Examples

    # HTTP transport
    auth_header = Frame.get_req_header(frame, "authorization")
    # => "Bearer token123"

    # Non-HTTP transport or missing header
    auth_header = Frame.get_req_header(frame, "authorization")
    # => nil

# `new`

```elixir
@spec new(assigns :: Enumerable.t()) :: t()
```

Creates a new frame with optional initial assigns.

## Examples

    iex> Frame.new()
    %Frame{assigns: %{}, initialized: false}

    iex> Frame.new(%{user: "alice"})
    %Frame{assigns: %{user: "alice"}, initialized: false}

# `put_pagination_limit`

```elixir
@spec put_pagination_limit(t(), non_neg_integer()) :: t()
```

Sets the pagination limit for listing operations.

This limit is used by handlers when returning lists of tools, prompts, or resources
to control the maximum number of items returned in a single response. When the limit
is set and the total number of items exceeds it, the response will include a
`nextCursor` field for pagination.

## Examples

    # Set pagination limit to 10 items per page
    frame = Frame.put_pagination_limit(frame, 10)

    # The limit is stored in private data
    frame.private.pagination_limit
    # => 10

# `put_private`

```elixir
@spec put_private(t(), Enumerable.t()) :: t()
```

# `put_private`

```elixir
@spec put_private(t(), atom(), any()) :: t()
```

Sets or updates private session data in the frame.

Private data is used for framework-internal session context that persists
across requests, similar to Plug.Conn.private.

## Examples

    # Set single private value
    frame = Frame.put_private(frame, :session_id, "abc123")

    # Set multiple private values
    frame = Frame.put_private(frame, %{
      session_id: "abc123",
      client_info: %{name: "my-client", version: "1.0.0"}
    })

# `put_request`

```elixir
@spec put_request(t(), map()) :: t()
```

Sets the current request being processed.

The request includes the request ID, method, and raw parameters before validation.

## Examples

    frame = Frame.put_request(frame, %{
      id: "req_123",
      method: "tools/call",
      params: %{"name" => "calculator", "arguments" => %{}}
    })

# `put_transport`

```elixir
@spec put_transport(t(), Enumerable.t()) :: t()
```

# `put_transport`

```elixir
@spec put_transport(t(), atom(), any()) :: t()
```

Sets or updates transport data in the frame.

Check `transport_t()` for reference.

## Examples

    # Set single transport value
    frame = Frame.put_transport(frame, :session_id, "abc123")

    # Set multiple transport values
    frame = Frame.put_transport(frame, %{
      session_id: "abc123",
      client_info: %{name: "my-client", version: "1.0.0"}
    })

# `register_prompt`

```elixir
@spec register_prompt(t(), String.t(), [prompt_opt]) :: t()
when prompt_opt:
       {:description, String.t() | nil}
       | {:arguments, map() | nil}
       | {:title, String.t() | nil}
```

Registers a prompt definition.

# `register_resource`

```elixir
@spec register_resource(t(), String.t(), [resource_opt]) :: t()
when resource_opt:
       {:title, String.t() | nil}
       | {:name, String.t() | nil}
       | {:description, String.t() | nil}
       | {:mime_type, String.t() | nil}
```

Registers a resource definition with a fixed URI.

For parameterized resources, use `register_resource_template/3` instead.

# `register_resource_template`

```elixir
@spec register_resource_template(t(), String.t(), [resource_template_opt]) :: t()
when resource_template_opt:
       {:title, String.t() | nil}
       | {:name, String.t()}
       | {:description, String.t() | nil}
       | {:mime_type, String.t() | nil}
```

Registers a resource template definition using a URI template (RFC 6570).

URI templates allow parameterized resources like `file:///{path}` or `db:///{table}/{id}`.

## Examples

    frame = Frame.register_resource_template(frame, "file:///{path}",
      name: "project_files",
      title: "Project Files",
      description: "Access files in the project directory"
    )

# `register_tool`

```elixir
@spec register_tool(t(), String.t(), [tool_opt]) :: t()
when tool_opt:
       {:description, String.t() | nil}
       | {:input_schema, map() | nil}
       | {:output_schema, map() | nil}
       | {:title, String.t() | nil}
       | {:annotations, map() | nil}
```

Registers a tool definition.

---

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