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" ) """ 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 ) """ 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(), Access.t()) :: 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