Represents the state of the MCP session. This is the state across the conversation and is the bridge between the various transports (HTTP, stdio) to persistence, even if stateless.
Summary
Functions
Set an allow-list of usable Prompts for the session
Set an allow-list of usable Resource Templates for the session
Set an allow-list of usable Tools for the session
Assign state to the session.
Assign state to the session.
Elicit input from the client.
Convenience to elicit a URL mode interaction. Blocks until the client responds.
Closes the connection for the session
Builds a new session with the provided session ID.
Send a notification to the client
Send a progress notification to the client
Send a ping to the client
A reference to the current request that another process, on any node, can
pass to notify_progress/4. It is a JSON-safe map, so it can travel in job
arguments, such as an Oban job's.
Fetch the current progress token if provided by the client
Sends response back to the stream
Send a JSON-RPC error response for a pending request.
Sets the log level for the SSE stream. Sets both for the current request for async tasks and the SSE stream
Whether the session's current request is using the MCP 2026-07-28
stateless-core protocol.
Subscribe the session to a resource.
Whether the client declared the Tasks extension on the current request, so
a tool may answer it with a Phantom.Tasks. The extension requires MCP
2026-07-28, and only the current request counts.
Unsubscribe the session to a resource.
Types
@type t() :: %Phantom.Session{ acknowledged_subscriptions: MapSet.t(), allowed_prompts: [String.t()], allowed_resource_templates: [String.t()], allowed_tools: [String.t()], assigns: map(), client_capabilities: %{ elicitation: false | map(), sampling: false | map(), roots: false | map(), ui: false | map() }, client_info: map(), close_after_complete: boolean(), id: binary(), last_event_id: String.t() | nil, pending_elicit: {Phantom.Elicit.t(), term()} | nil, pid: pid() | nil, pubsub: module(), request: Phantom.Request.t() | nil, requests: map(), router: module(), state: term() | nil, stream_fun: fun(), subscription_filter: map() | nil, subscription_id: String.t() | integer() | nil, subscriptions: map(), tracker: term(), transport_pid: pid() }
Functions
Set an allow-list of usable Prompts for the session
Set an allow-list of usable Resource Templates for the session
Set an allow-list of usable Tools for the session
Assign state to the session.
Assign state to the session.
@spec elicit(t(), Phantom.Elicit.t(), keyword()) :: {:ok, response :: map()} | t() | :not_supported | :error | :timeout
Elicit input from the client.
Two call patterns, with protocol-aware defaults that preserve historical behavior:
Inline blocking (
:awaittrue, or default under legacy) — returns{:ok, response}whereresponseis the client's JSON map ("action"and"content"keys), or:not_supported/:timeout/:error. Under legacy MCP protocols the call blocks via the open SSE stream. Under MCP2026-07-28the calling process waits: the request is answered with aninput_requiredresult, and the client's follow-up call, on any node, resumes the process and receives its eventual response.Re-entry (
:stateset, or default under stateless) — returns thesessionstruct with the pending elicit attached. The handler wraps it in the standard{:noreply, session}reply shape:{:noreply, Session.elicit(session, elicit, state: %{step: :got_input})}The dispatcher then converts to an
input_requiredresult (stateless) or runs through the SSE elicit round-trip + handler re-invocation (legacy). On resume, the handler is re-entered withsession.statepopulated to whatever you passed as:state. Structure the handler with a function-head clause that matches on%Session{state: %{...}}.
Without :state the call blocks inline under every protocol, so code that
pattern-matches {:ok, response} works unchanged under MCP 2026-07-28.
The exception is a handler that runs in the request's own process, as
resource handlers do: it cannot wait on itself, so it re-enters.
Pick based on style preference:
# Inline — returns the client's response.
def my_tool(_params, session) do
{:ok, %{"action" => "accept", "content" => %{"choice" => c}}} =
Session.elicit(session, elicit, await: true)
{:reply, Tool.text("got #{c}"), session}
end
# Re-entry — the handler is invoked again with session.state populated
def my_tool(%{"choice" => c}, %Session{state: %{step: :got_choice}} = session) do
{:reply, Tool.text("got #{c}"), session}
end
def my_tool(params, session) do
{:noreply, Session.elicit(session, elicit, state: %{step: :got_choice})}
endOptions:
:await—trueto force inline blocking:state— value placed onsession.stateon re-entry; forces re-entry mode regardless of protocol:timeout— max blocking time in ms (:awaitmode only; default: 5 minutes)
@spec elicit_url(t(), url :: String.t(), message :: String.t(), keyword()) :: {:ok, response :: map()} | :not_supported | :error | :timeout
Convenience to elicit a URL mode interaction. Blocks until the client responds.
Closes the connection for the session
Builds a new session with the provided session ID.
This is used for adapters such as Phantom.Plug. If a
session ID is not provided, it will generate one using UUIDv7.
Send a notification to the client
Send a progress notification to the client
the progress and total can be a integer or float, but must be ever-increasing.
the total is optional.
Pass the session, or a reference from progress_ref/1 to notify from
another process or node.
https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/progress
Send a ping to the client
A reference to the current request that another process, on any node, can
pass to notify_progress/4. It is a JSON-safe map, so it can travel in job
arguments, such as an Oban job's.
Returns nil when the client did not ask for progress or the session has
no PubSub, and notify_progress/4 does nothing with nil.
args = %{report_id: id, progress: Phantom.Session.progress_ref(session)}
# In the job, on any node:
Phantom.Session.notify_progress(args["progress"], 50, 100, "Halfway")
Fetch the current progress token if provided by the client
Sends response back to the stream
This should likely be used in conjunction with:
Phantom.Tool.response(payload)Phantom.Resource.response(payload)Phantom.Prompt.response(payload)
For example:
session_pid = session.pid
request_id = request.id
Task.async(fn ->
Session.respond(
session_pid,
request_id,
Phantom.Tool.audio(
File.read!("priv/static/game-over.wav"),
mime_type: "audio/wav"
)
)
end)
See respond/2
Send a JSON-RPC error response for a pending request.
Used by async tool handlers (running in a Task) to finalize a request with a protocol-level error rather than a Tool.error result.
@spec set_log_level(t(), Phantom.Request.t(), String.t()) :: :ok | :error
Sets the log level for the SSE stream. Sets both for the current request for async tasks and the SSE stream
Whether the session's current request is using the MCP 2026-07-28
stateless-core protocol.
@spec subscribe_to_resource(t(), Phantom.Router.resolved_resource() | String.t()) :: :ok | :error
Subscribe the session to a resource.
This is used by the MCP Router when the client requests to subscribe to the provided resource.
Whether the client declared the Tasks extension on the current request, so
a tool may answer it with a Phantom.Tasks. The extension requires MCP
2026-07-28, and only the current request counts.
Unsubscribe the session to a resource.
This is used by the MCP Router when the client requests to subscribe to the provided resource.