Current section

Files

Jump to
phantom_mcp lib phantom request.ex
Raw

lib/phantom/request.ex

defmodule Phantom.Request do
@moduledoc "Standard requests and responses for the MCP protocol"
defstruct [:id, :type, :method, :params, :response, :spec]
@opaque t :: %__MODULE__{
id: String.t(),
type: String.t(),
method: String.t(),
params: map(),
response: map(),
spec: Phantom.ResourceTemplate.t() | Phantom.Tool.t() | Phantom.Prompt.t()
}
@connection -32000
@resource_not_found -32002
@invalid_request -32600
@method_not_found -32601
@invalid_params -32602
@internal_error -32603
@parse_error -32700
@url_elicitation_required -32042
import Phantom.Utils
alias Phantom.Session
@doc "Invalid request"
def invalid(message \\ nil) do
%{code: @invalid_request, message: message || "Invalid request"}
end
@doc "Invalid request due to bad parameters"
def invalid_params(data), do: %{code: @invalid_params, message: "Invalid Params", data: data}
def invalid_params, do: %{code: @invalid_params, message: "Invalid Params"}
@doc "Invalid request due to parsing error"
def parse_error(message \\ nil) do
%{code: @parse_error, message: message || "Parsing error"}
end
@doc "Invalid request due to no streaming connection being available"
def closed(message \\ nil) do
%{code: @connection, message: message || "Connection closed"}
end
@doc "Server encountered an issue"
def internal_error(message \\ nil) do
%{code: @internal_error, message: message || "Internal server error"}
end
@doc "The method is not implemented or found"
def not_found(message \\ nil),
do: %{code: @method_not_found, message: message || "Method not found"}
@doc "The resource is not found"
def resource_not_found(data),
do: %{code: @resource_not_found, data: data, message: "Resource not found"}
@doc "Error indicating URL mode elicitation is required before retrying"
def url_elicitation_required(elicitations) when is_list(elicitations) do
%{
code: @url_elicitation_required,
message: "This request requires more information.",
data: %{elicitations: Enum.map(elicitations, &Phantom.Elicit.to_json/1)}
}
end
@doc "Elicitation complete notification"
def elicitation_complete(elicitation_id) do
%{
jsonrpc: "2.0",
method: "notifications/elicitation/complete",
params: %{elicitationId: elicitation_id}
}
end
@doc false
def build(nil), do: nil
def build(%{"jsonrpc" => "2.0", "method" => method} = request)
when is_binary(method) do
{:ok,
struct!(__MODULE__,
params: request["params"] || %{},
method: method,
id: request["id"]
)}
end
def build(%{"jsonrpc" => "2.0", "result" => result} = response)
when is_map(result) do
{:ok,
struct!(__MODULE__,
response: result,
id: response["id"]
)}
end
def build(request) do
{:error, struct!(__MODULE__, id: request["id"], response: error(request["id"], invalid()))}
end
@doc false
def to_json(%__MODULE__{} = request) do
%{
"jsonrpc" => "2.0",
"method" => request.method,
"id" => request.id,
"params" => request.params
}
end
@doc "Ping request"
def ping() do
%{jsonrpc: "2.0", method: "ping", id: UUIDv7.generate()}
end
@doc "An empty response"
def empty() do
%{jsonrpc: "2.0", result: ""}
end
@doc false
def result(%__MODULE__{} = request, type, result) do
%{request | type: type, response: %{id: request.id, jsonrpc: "2.0", result: result}}
end
@doc "Response error"
def error(id \\ nil, error) do
%{jsonrpc: "2.0", error: error, id: id}
end
@doc false
def completion_response({:reply, results, session}, _session) do
{:reply, completion_response(results), session}
end
def completion_response({:error, error}, session) do
{:error, error, session}
end
def completion_response({:noreply, session}, _session) do
{:noreply, session}
end
def completion_response(results) when is_list(results) do
%{
completion: %{
values: Enum.take(List.wrap(results), 100),
hasMore: length(results) > 100
}
}
end
def completion_response(%{} = results) do
%{
completion:
remove_nils(%{
values: Enum.take(List.wrap(results[:values]), 100),
total: results[:total],
hasMore: results[:has_more] || false
})
}
end
@doc false
def resource_response({:error, reason}, _uri, session) do
{:error, reason, session}
end
def resource_response({:noreply, _} = result, _uri, _session), do: result
def resource_response({:error, _reason, %Session{}} = result, _uri, _session) do
result
end
def resource_response(nil, uri, session) do
{:error, resource_not_found(%{uri: uri}), session}
end
def resource_response({:reply, nil, %Session{} = session}, uri, _session) do
{:error, resource_not_found(%{uri: uri}), session}
end
def resource_response({:reply, results, %Session{} = session}, _uri, _session) do
{:reply, Phantom.Resource.response(results), session}
end
@doc "Resource updated notification"
def resource_updated(content) do
%{jsonrpc: "2.0", method: "notifications/resources/updated", params: content}
end
@doc "Tools List updated notification"
def tools_updated do
%{jsonrpc: "2.0", method: "notifications/tools/list_changed"}
end
@doc "Prompts List updated notification"
def prompts_updated do
%{jsonrpc: "2.0", method: "notifications/prompts/list_changed"}
end
@doc "Resources List updated notification"
def resources_updated do
%{jsonrpc: "2.0", method: "notifications/resources/list_changed"}
end
@doc "A generic notifiation"
def notify(content) do
%{jsonrpc: "2.0", method: "notifications/message", params: content}
end
@doc "Progress notifiation"
def notify_progress(progress_token, progress, total) do
%{
jsonrpc: "2.0",
method: "notifications/progress",
params:
remove_nils(%{
progressToken: progress_token,
progress: progress,
total: total
})
}
end
end