Current section
Files
Jump to
Current section
Files
lib/phantom/resource_template.ex
defmodule Phantom.ResourceTemplate do
@moduledoc """
The Model Context Protocol (MCP) provides a standardized way for
servers to expose resources to clients. Resources allow servers to
share data that provides context to language models, such as files,
database schemas, or application-specific information. Each resource
is uniquely identified by a URI.
https://modelcontextprotocol.io/specification/2025-03-26/server/resources
"""
import Phantom.Utils
@enforce_keys ~w[name handler function path router scheme uri uri_template]a
defstruct [
:name,
:description,
:function,
:completion_function,
:handler,
:mime_type,
:path,
:router,
:scheme,
:size,
:uri,
:uri_template,
meta: %{}
]
@type t :: %__MODULE__{
name: String.t(),
description: String.t(),
function: atom(),
completion_function: atom(),
handler: module(),
mime_type: String.t(),
path: String.t(),
router: module(),
scheme: String.t(),
size: pos_integer(),
meta: map(),
uri: URI.t(),
uri_template: String.t()
}
@type json :: %{
required(:uri) => String.t(),
required(:name) => String.t(),
optional(:description) => String.t(),
optional(:mimeType) => String.t(),
optional(:size) => pos_integer()
}
@spec build(map() | Keyword.t()) :: t()
@doc """
Build a resource_template spec
The `Phantom.Router.resource/3` macro will build these specs.
- `:name` - The name of the resource template.
- `:uri` - The URI template of the resource in the style of `Plug.Router`, including the scheme.
For example, you can define a path like `"myapp:///some/path/:project_id/:id` which
will be parsed to include path params `%{"project_id" => _, "id" => _}`. The scheme can be
`"https"`, `"git"`, `"file"`, or custom, eg `"myapp"`.
- `:description` - The description of the resource and when to use it.
- `:handler` - The module to call.
- `:function` - The function to call on the handler module.
- `:completion_function` - The function to call on the handler module that will provide possible completion results.
- `:mime_type` - the MIME type of the results.
- `:router` - The Router module that will capture the URIs and route resources by URI to functions.
This is constructed by the `Phantom.Router.resource/3` macro automatically as
`MyApp.MyMCPRouter.ResourceRouter.{Scheme}`. The module does not need to exist at the time of
building it-- it will be generated when added by `Phantom.Cache.add_resource_template/2`
or by the `Phantom.Router.resource/3` macro.
"""
def build(attrs) do
uri =
case attrs[:uri] do
%URI{} = uri -> {:ok, uri}
uri when is_binary(uri) -> URI.new(uri)
end
uri =
case uri do
{:ok, %URI{path: path, scheme: scheme} = uri}
when is_binary(path) and is_binary(scheme) ->
uri
{:ok, uri} ->
raise "Provided an invalid URI.\nResource URIs must contain a path and a scheme.\nProvided: #{URI.to_string(uri)}"
{:error, invalid} ->
raise "Provided an invalid URI.\nProvided: #{inspect(attrs[:uri])}\nError at: #{inspect(invalid)}"
end
struct!(
__MODULE__,
attrs
|> Map.new()
|> Map.merge(%{
name: attrs[:name] || to_string(attrs[:function]),
scheme: attrs[:scheme] || uri.scheme,
path: attrs[:path] || uri.path,
uri_template: "#{uri.scheme}://#{to_uri_6570(uri.path)}"
})
)
end
@spec to_json(t()) :: json()
@doc """
Represent a ResourceTemplate spec as json when listing the available resources to clients.
"""
def to_json(%__MODULE__{} = resource) do
remove_nils(%{
uriTemplate: resource.uri_template,
name: resource.name,
size: resource.size,
description: resource.description,
mimeType: resource.mime_type
})
end
defp to_uri_6570(str) do
# this is not a total 6570-compliant URI template.
String.replace(str, ~r/:\w*/, fn ":" <> var -> "{#{var}}" end)
end
end