Phantom.Router behaviour (phantom_mcp v0.3.2)
View SourceA DSL for defining MCP servers. This module provides functions that define tools, resources, and prompts.
See Phantom
for usage examples.
Telemetry
Telemetry is provided with these events:
[:phantom, :dispatch, :start]
with meta:~w[method params request session]a
[:phantom, :dispatch, :stop]
with meta:~w[method params request result session]a
[:phantom, :dispatch, :exception]
with meta:~w[method kind reason stacktrace params request session]a
Summary
Callbacks
When the connection is opening, this callback will be invoked.
When the connection is closing, this callback will be invoked.
Return the instructions for the MCP server for a given session. This is retrieved when the client is initializing with the server.
List resources available to the client.
Return the server information for the MCP server for a given session. This is retrieved when the client is initializing with the server.
When the session is terminating, this callback will be invoked. Termination is when the client indicates they are finished with the MCP session.
Functions
Define a prompt that can be retrieved by the MCP client.
Reads the resource given its URI, primarily for embedded resources.
Define a resource that can be read by the MCP client.
Constructs a response map for the given resource with the provided parameters. This function is provided to your MCP Router that accepts the session instead.
Define a tool that can be called by the MCP client.
Callbacks
@callback connect(Phantom.Session.t(), %{ optional(:headers) => Plug.Conn.headers(), optional(:params) => Plug.Conn.query_params() }) :: {:ok, Phantom.Session.t()} | {:unauthorized | 401, www_authenticate_header :: Phantom.Plug.www_authenticate()} | {:forbidden | 403, message :: String.t()} | {:error, any()}
When the connection is opening, this callback will be invoked.
This is critical for authentication and authorization.
{:ok, session}
- The session is authenticated and authorized.{:unauthorized | 401, www_authenticate_header}
- The session is not authenticated. Thewww_authenticate_header
should reveal to the client how to authenticate. This can either be a string to represent a built header, or a map that is passed intoPhantom.Plug.www_authenticate/1
to build the header.{:forbidden | 403, message}
- The session is not authorized. For example, the user is authenticated, but lacks the account permissions to access the MCP server.{:error, message}
- The connection should be rejected for any other reason.
@callback disconnect(Phantom.Session.t()) :: {:ok, Phantom.Session.t()} | any()
When the connection is closing, this callback will be invoked.
The return value is largely ignored. This is a lifecycle event that will likely happen very often.
This could be helpful if you wanted to emit a side-effect when the connection closes or
modify the session. Consider hooking into the [:phantom, :plug, :request, :disconnect]
telemetry event instead. The telemetry event will receive the modified session if implemented.
@callback instructions(Phantom.Session.t()) :: {:ok, String.t()}
Return the instructions for the MCP server for a given session. This is retrieved when the client is initializing with the server.
By default, it will return the compiled :instructions
provided to use Phantom.Router
, however
if you need the instructions to be dynamic based on the session, you may implement this
callback and return {:ok, "my instructions"}
. Any other shape will result in no instructions.
@callback list_resources(String.t() | nil, Phantom.Session.t()) :: {:reply, Phantom.Resource.list_response(), Phantom.Session.t()} | {:noreply, Phantom.Session.t()} | {:error, any(), Phantom.Session.t()}
List resources available to the client.
This will expect the response to use Phantom.Resource.list/2
as the result.
You may also want to leverage resource_for/3
and Phantom.Resource.resource_link/3
to construct the response. See Phantom
for an exmaple.
Remember to check for allowed resources according to session.allowed_resource_templates
@callback server_info(Phantom.Session.t()) :: {:ok, %{name: String.t(), version: String.t()}} | {:error, any()}
Return the server information for the MCP server for a given session. This is retrieved when the client is initializing with the server.
By default, it will return the static :name
and :vsn
provided to use Phantom.Router
, however
if you need the instructions to be dynamic based on the session, you may implement this
callback and return {:ok, %{name: "my name", version: "my version"}
. Any other shape will result
in no server information.
@callback terminate(Phantom.Session.t()) :: {:ok, any()} | {:error, any()}
When the session is terminating, this callback will be invoked. Termination is when the client indicates they are finished with the MCP session.
The callback will be invoked and should return {:ok, _}
or {:error, _}
to indicate
success or not in terminating the session. Consider hooking into the
[:phantom, :plug, :request, :terminate]
telemetry event for side-effects.
Functions
Define a prompt that can be retrieved by the MCP client.
Examples
prompt :summarize,
description: "A text prompt",
completion_function: :summarize_complete,
arguments: [
%{
name: "text",
description: "The text to summarize",
},
%{
name: "resource",
description: "The resource to summarize",
}
]
)
# ...
require Phantom.Prompt, as: Prompt
def summarize(args, _request, session) do
{:reply, Prompt.response([
assistant: Prompt.text("You're great"),
user: Prompt.text("No you're great!")
], session}
end
def summarize_complete("text", _typed_value, session) do
{:reply, ["many values"], session}
end
def summarize_complete("resource", _typed_value, session) do
# list of IDs
{:reply, ["123"], session}
end
@spec read_resource(Phantom.Session.t(), module(), URI.t()) :: {:ok, uri_string :: String.t(), Phantom.Resource.blob_content() | Phantom.Resource.text_content()} | {:error, error_response :: map()}
Reads the resource given its URI, primarily for embedded resources.
This is available on your router as: MyApp.MCP.Router.read_resource/3
that
accepts the session, resource_name, and path params.
For example:
iex> MyApp.MCP.Router.read_resource(session, :my_resource, id: 321)
{:ok, "myapp:///resources/123", %{
blob: "abc123"
uri: "myapp:///resources/123",
mimeType: "audio/wav",
name: "Some audio",
title: "Super audio"
}}
Define a resource that can be read by the MCP client.
Examples
resource "app:///studies/:id", MyApp.MCP, :read_study,
description: "A study",
mime_type: "application/json"
# ...
require Phantom.Resource, as: Resource
def read_study(%{"id" => id}, _request, session) do
{:reply, Response.response(
Response.text("IO.puts \"Hi\"")
), session}
end
Constructs a response map for the given resource with the provided parameters. This function is provided to your MCP Router that accepts the session instead.
For example
iex> MyApp.MCP.Router.resource_uri(session, :my_resource, id: 123)
{:ok, "myapp:///my-resource/123"}
iex> MyApp.MCP.Router.resource_uri(session, :my_resource, foo: "error")
{:error, :invalid_params}
iex> MyApp.MCP.Router.resource_uri(session, :unknown, id: 123)
{:error, :router_not_found}
Define a tool that can be called by the MCP client.
Examples
tool :local_echo,
description: "A test that echos your message",
# or supply a `@description` before defining the tool
input_schema: %{
required: [:message],
properties: %{
message: %{
type: "string",
description: "message to echo"
}
}
}
### handled by your function syncronously:
require Phantom.Tool, as: Tool
def local_echo(params, session) do
# Maps will be JSON-encoded and also provided
# as structured content.
{:reply, Tool.text(params), session}
end
# Or asyncronously:
require Phantom.Tool, as: Tool
def local_echo(params, session) do
Task.async(fn ->
Process.sleep(1000)
Session.respond(session, Tool.text(params))
end)
{:noreply, session}
end