Anubis.Server.Frame (anubis_mcp v0.14.0)

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"

Summary

Functions

Assigns a value or multiple values to the frame.

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

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

Clears the current request from the frame.

Clears all session-specific private data from the frame.

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

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

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

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

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

Gets a query parameter value from HTTP transport.

Gets a request header value from HTTP transport.

Creates a new frame with optional initial assigns.

Sets the pagination limit for listing operations.

Sets or updates private session data in the frame.

Sets the current request being processed.

Sets or updates transport data in the frame.

Registers a prompt definition.

Registers a resource definition. THis also supports resource templates (via URI templates).

Registers a tool definition.

Types

http_t()

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

private_t()

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

request_t()

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

server_component_t()

stdio_t()

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

t()

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

transport_t()

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

Functions

assign(frame, assigns)

@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(frame, key, value)

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

assign_new(frame, key, fun)

@spec assign_new(t(), key :: atom(), value_fun :: (-> 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(frame)

@spec clear_components(t()) :: t()

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

clear_request(frame)

@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(frame)

@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(frame)

@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(frame)

@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(frame)

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

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

get_mcp_session_id(frame)

@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_version(frame)

@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(frame, key)

@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(frame, name)

@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(assigns \\ %{})

@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(frame, limit)

@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(frame, private)

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

put_private(frame, key, value)

@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(frame, request)

@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(frame, transport)

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

put_transport(frame, key, value)

@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(frame, name, opts)

@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(frame, uri, opts)

@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. THis also supports resource templates (via URI templates).

register_tool(frame, name, opts)

@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.