Current section
Files
Jump to
Current section
Files
lib/phantom/resource.ex
defmodule Phantom.Resource 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
```mermaid
sequenceDiagram
participant Client
participant Server
Note over Client,Server: Resource Discovery
Client->>Server: resources/list
Server-->>Client: List of resources
Note over Client,Server: Resource Access
Client->>Server: resources/read
Server-->>Client: Resource contents
Note over Client,Server: Subscriptions
Client->>Server: resources/subscribe
Server-->>Client: Subscription confirmed
Note over Client,Server: Updates
Server--)Client: notifications/resources/updated
Client->>Server: resources/read
Server-->>Client: Updated contents
```
"""
import Phantom.Utils
@type response :: %{
contents: [blob_content() | text_content()]
}
@type blob_content :: %{
required(:blob) => binary(),
required(:uri) => String.t(),
optional(:mimeType) => String.t(),
optional(:name) => String.t(),
optional(:title) => String.t()
}
@type text_content :: %{
required(:text) => binary(),
required(:uri) => String.t(),
optional(:mimeType) => String.t(),
optional(:name) => String.t(),
optional(:title) => String.t()
}
@type resource_link :: %{
uri: String.t(),
name: String.t(),
description: String.t(),
mimeType: String.t()
}
# TODO: not ready
@doc false
def updated(uri), do: %{uri: uri}
@doc """
Resource as binary content
The `:uri` and `:mime_type` will be fetched from the current resource template
within the scope of the request if not provided, but you will need to provide
the rest.
- `binary` - Binary data. This will be base64-encoded by Phantom.
- `:uri` (required) Unique identifier for the resource
- `:name` (optional) The name of the resource.
- `:title` (optional) human-readable name of the resource for display purposes.
- `:description` (optional) Description
- `:mime_type` (optional) MIME type
- `:size` (optional) Size in bytes
For example:
Phantom.Resource.blob(
File.read!("foo.png"),
uri: "test://my-foos/123",
mime_type: "image/png"
)
"""
@spec blob(binary(), Keyword.t() | map()) :: blob_content()
defmacro blob(binary, attrs \\ []) do
mime_type =
get_var(attrs, :mime_type, [:spec, :mime_type], __CALLER__, "application/octet-stream")
uri = get_var(attrs, :uri, [:params, "uri"], __CALLER__)
quote bind_quoted: [
uri: uri,
mime_type: mime_type,
binary: binary,
attrs: Macro.escape(attrs)
] do
remove_nils(%{
blob: Base.encode64(binary),
uri: uri,
mimeType: mime_type,
name: attrs[:name],
title: attrs[:title],
size: attrs[:size]
})
end
end
@doc """
Resource as text content
The `:uri` and `:mime_type` will be fetched from the current resource template
within the scope of the request if not provided, but you will need to provide
the rest.
- `text` - Text data. If a map, then it will be encoded into JSON and `:mime_type` will be set accordingly unless provided.
- `:uri` (required) Unique identifier for the resource
- `:name` (optional) The name of the resource.
- `:title` (optional) human-readable name of the resource for display purposes.
- `:description` (optional) Description
- `:mime_type` (optional) MIME type. Defaults to `"text/plain"`
- `:size` (optional) Size in bytes
For example:
Phantom.ResourceTemplate.text(
"## Why hello there",
uri: "test://obi-wan-quotes/hello-there",
mime_type: "text/markdown"
)
Phantom.ResourceTemplate.text(
%{why: "hello there"},
uri: "test://my-foos/json",
# mime_type: "application/json" # set by Phantom
)
"""
@spec text(String.t() | map, Keyword.t() | map()) :: text_content()
defmacro text(text, attrs \\ %{}) do
mime_type = get_var(attrs, :mime_type, [:spec, :mime_type], __CALLER__, "text/plain")
uri = get_var(attrs, :uri, [:params, "uri"], __CALLER__)
quote bind_quoted: [
text: text,
uri: uri,
mime_type: mime_type,
attrs: Macro.escape(attrs)
] do
tmp_text = if is_map(t = text), do: JSON.encode!(t), else: t
json_mime = if is_map(text), do: "application/json"
remove_nils(%{
text: tmp_text,
uri: uri,
mimeType: json_mime || mime_type || "text/plain",
name: attrs[:name],
title: attrs[:title],
size: attrs[:size]
})
end
end
@doc "Formats the response from an MCP Router to the MCP specification"
def response(%{contents: _} = results), do: results
def response(results) do
%{contents: List.wrap(results)}
end
@type list_response :: %{nextCursor: String.t() | nil, resources: [resource_link()]}
@spec list([resource_link()], cursor :: String.t() | nil) :: list_response()
@doc "Formats the response from the MCP Router to the MCP specification for listing resources"
def list(resource_links, next_cursor) do
%{resources: List.wrap(resource_links), nextCursor: next_cursor}
end
@doc """
Formats a resource_template and the provided attributes as a resource link. This is
typicaly used when listing resources or when tools embed a resource_link within its result.
"""
@spec resource_link(string_uri :: String.t(), Phantom.ResourceTemplate.t(), map()) ::
resource_link()
def resource_link(uri, %Phantom.ResourceTemplate{} = resource_template, attrs \\ %{}) do
remove_nils(%{
uri: uri,
mimeType: attrs[:mime_type] || resource_template.mime_type,
description: attrs[:description] || resource_template.description,
name: attrs[:name] || resource_template.name
})
end
end