Current section
Files
Jump to
Current section
Files
lib/phantom/elicit.ex
defmodule Phantom.Elicit do
@moduledoc """
The Model Context Protocol (MCP) provides a standardized way for
servers to request additional information from users through the
client during interactions. This flow allows clients to maintain
control over user interactions and data sharing while enabling
servers to gather necessary information dynamically. Servers
request structured data from users with JSON schemas to validate
responses.
> #### Error {: .error}
>
> Note: this is not yet tested
https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation
```mermaid
sequenceDiagram
participant User
participant Client
participant Server
Note over Server,Client: Server initiates elicitation
Server->>Client: elicitation/create
Note over Client,User: Human interaction
Client->>User: Present elicitation UI
User-->>Client: Provide requested information
Note over Server,Client: Complete request
Client-->>Server: Return user response
Note over Server: Continue processing with new information
```
"""
@enforce_keys ~w[message requested_schema]a
defstruct [
:message,
:requested_schema
]
@type string_property :: %{
name: String.t(),
required: boolean(),
type: :string,
title: String.t(),
description: String.t(),
min_length: pos_integer(),
max_length: pos_integer(),
pattern: String.t() | Regex.t(),
format: :email | :uri | :date | :datetime
}
@type enum_property :: %{
name: String.t(),
required: boolean(),
type: :string,
title: String.t(),
description: String.t(),
enum: [{value :: String.t(), name :: String.t()}]
}
@type boolean_property :: %{
name: String.t(),
required: boolean(),
type: :boolean,
title: String.t(),
description: String.t(),
default: boolean()
}
@type number_property :: %{
name: String.t(),
required: boolean(),
type: :number | :integer,
title: String.t(),
description: String.t(),
minimum: pos_integer(),
maximum: pos_integer()
}
@type t :: %__MODULE__{
message: String.t(),
requested_schema: [
number_property() | boolean_property() | enum_property() | string_property()
]
}
@type json :: %{
message: String.t(),
requestedSchema: %{
type: String.t(),
required: [String.t()],
properties: %{String.t() => map()}
}
}
@spec build(%{
message: String.t(),
requested_schema: [
number_property() | boolean_property() | enum_property() | string_property()
]
}) :: t
def build(attrs) do
%{
struct!(__MODULE__, attrs)
| requested_schema: attrs[:requested_schema] |> List.wrap() |> Enum.map(&build_property/1)
}
end
defp build_property(%{type: :string} = attrs) do
attrs =
Map.take(
attrs,
~w[name required type title description min_length max_length pattern format]a
)
if format = attrs[:format] do
format in ~w[email uri date date_time]a || raise "Invalid format in string property"
end
attrs
end
defp build_property(%{enum: enum} = attrs) when is_list(enum) do
attrs
|> Map.take(~w[name required type title description enum]a)
|> Map.put(:type, :string)
end
defp build_property(%{type: :boolean} = attrs) do
Map.take(
attrs,
~w[name required type title description default]a
)
end
@integer ~w[integer number]a
defp build_property(%{type: type} = attrs) when type in @integer do
Map.take(
attrs,
~w[name required type title description minimum maximum]a
)
end
def to_json(%__MODULE__{} = elicit) do
%{
message: elicit.message,
requestedSchema: %{
type: "object",
required:
Enum.reduce(elicit.requested_schema, [], fn property, acc ->
if property.required, do: [property.name | acc], else: acc
end),
properties:
Enum.reduce(elicit.requested_schema, %{}, fn property, acc ->
property = Map.drop(property, [:required])
{name, attrs} = Map.pop(property, :name)
Map.put(
acc,
name,
Enum.reduce(attrs, %{}, fn
{:min_length, v}, acc ->
Map.put(acc, :minLength, v)
{:max_length, v}, acc ->
Map.put(acc, :maxLength, v)
{:pattern, %Regex{} = v}, acc ->
Map.put(acc, :pattern, Regex.source(v))
{:format, :date_time}, acc ->
Map.put(acc, :format, "date-time")
{:enum, v}, acc ->
acc
|> Map.put(:enum, Enum.map(v, &elem(&1, 0)))
|> Map.put(:enumNames, Enum.map(v, &elem(&1, 1)))
{k, v}, acc ->
Map.put(acc, k, v)
end)
)
end)
}
}
end
end