Packages

Model Context Protocol (MCP) implementation in Elixir with Phoenix integration

Retired package: Release invalid - Accidental version. Use 0.3.0 instead.

Current section

Files

Jump to
backplane_mcp_protocol lib backplane mcp_protocol server component resource.ex
Raw

lib/backplane/mcp_protocol/server/component/resource.ex

defmodule Backplane.McpProtocol.Server.Component.Resource do
@moduledoc """
Defines the behaviour for MCP resources.
Resources represent data that can be read by the client, such as files,
documents, or any other content. Each resource is identified by a URI
and can provide content in various formats.
## Example
defmodule MyServer.Resources.Documentation do
@behaviour Backplane.McpProtocol.Server.Behaviour.Resource
alias Backplane.McpProtocol.Server.{Frame, Response}
alias Backplane.McpProtocol.MCP.Error
@impl true
def uri, do: "file:///docs/readme.md"
@impl true
def name, do: "Project README"
@impl true
def description, do: "The main documentation for this project"
@impl true
def mime_type, do: "text/markdown"
@impl true
def read(_params, frame) do
case File.read("README.md") do
{:ok, content} ->
# Can track access in frame
new_frame = Frame.assign(frame, :last_resource_access, DateTime.utc_now())
{:reply, Response.text(Response.resource(), content), new_frame}
{:error, reason} ->
{:error, Error.domain_error("Failed to read README: \#{inspect(reason)}"), frame}
end
end
end
## Example with URI template (parameterized resource)
defmodule MyServer.Resources.UserDoc do
use Backplane.McpProtocol.Server.Component,
type: :resource,
uri_template: "file:///docs/{user}/{filename}"
alias Backplane.McpProtocol.Server.Response
@impl true
def read(%{"params" => %{"user" => user, "filename" => name}}, frame) do
path = Path.join(["docs", user, name])
case File.read(path) do
{:ok, content} ->
{:reply, Response.text(Response.resource(), content), frame}
{:error, _} ->
{:error, Backplane.McpProtocol.MCP.Error.resource(:not_found, %{message: "no such file"}), frame}
end
end
end
Variables in `uri_template` follow RFC 6570 (Level 1 — simple `{var}` expansion).
Extracted variables are delivered to `read/2` as the `"params"` key of the first argument.
## Example with dynamic content
defmodule MyServer.Resources.SystemStatus do
@behaviour Backplane.McpProtocol.Server.Behaviour.Resource
@impl true
def uri, do: "system://status"
@impl true
def name, do: "System Status"
@impl true
def description, do: "Current system status and metrics"
@impl true
def mime_type, do: "application/json"
@impl true
def read(_params, frame) do
status = %{
uptime: System.uptime(),
memory: :erlang.memory(),
user_id: frame.assigns[:user_id],
timestamp: DateTime.utc_now()
}
{:reply, Response.json(Response.resource(), status), frame}
end
end
"""
alias Backplane.McpProtocol.MCP.Error
alias Backplane.McpProtocol.Server.Frame
alias Backplane.McpProtocol.Server.Response
@type params :: map()
@type content :: binary() | String.t()
@type t :: %__MODULE__{
uri: String.t() | nil,
uri_template: String.t() | nil,
name: String.t(),
description: String.t() | nil,
mime_type: String.t(),
handler: module | nil,
title: String.t() | nil,
scopes: [String.t()]
}
defstruct [
:uri,
:uri_template,
:name,
description: nil,
mime_type: "text/plain",
handler: nil,
title: nil,
scopes: []
]
@doc """
Returns the URI that identifies this resource.
The URI should be unique within the server and follow standard URI conventions.
Common schemes include:
- `file://` for file-based resources
- `http://` or `https://` for web resources
- Custom schemes for application-specific resources
Note: Either `uri/0` or `uri_template/0` must be implemented, but not both.
"""
@callback uri() :: String.t()
@doc """
Returns the URI template that identifies this resource template.
URI templates follow RFC 6570 syntax and allow parameterized resource URIs.
For example: `file:///{path}` or `db:///{table}/{id}`
Note: Either `uri/0` or `uri_template/0` must be implemented, but not both.
"""
@callback uri_template() :: String.t()
@doc """
Returns the `name` that identifies this resource.
Intended for programmatic or logical use, but used as a
display name in past specs or fallback (if title isn't present).
"""
@callback name() :: String.t()
@doc """
Returns the title that identifies this resource.
Intended for UI and end-user contexts — optimized to be human-readable and easily understood,
even by those unfamiliar with domain-specific terminology.
If not provided, the name should be used for display.
"""
@callback title() :: String.t()
@doc """
Returns the MIME type of the resource content.
Common MIME types:
- `text/plain` for plain text
- `text/markdown` for Markdown
- `application/json` for JSON data
- `text/html` for HTML
- `application/octet-stream` for binary data
"""
@callback mime_type() :: String.t()
@doc """
Returns the description of this resource.
The description helps AI assistants understand what data the resource provides.
If not provided, the module's `@moduledoc` will be used automatically.
## Examples
def description do
"Application configuration settings"
end
# With dynamic content
def description do
{uptime_ms, _} = :erlang.statistics(:wall_clock)
"System metrics (uptime: \#{div(uptime_ms, 1000)}s)"
end
"""
@callback description() :: String.t()
@doc """
Reads the resource content.
## Parameters
- `params` - Optional parameters from the client (typically empty for resources)
- `frame` - The server frame containing context and state
## Return Values
- `{:reply, %Response{}, frame}` - Resource read successfully
- `{:noreply, frame}` - No reply needed
- `{:error, %Error{}, frame}` - Failed to read resource
## Building Responses
Use `Response.resource/0` to create a resource response, then set content
with the appropriate builder:
- `Response.text/2` for text content (plain text, markdown, etc.)
- `Response.json/2` for JSON data (automatically encoded)
- `Response.blob/2` for binary data
"""
@callback read(params :: params(), frame :: Frame.t()) ::
{:reply, response :: Response.t(), new_state :: Frame.t()}
| {:noreply, new_state :: Frame.t()}
| {:error, error :: Error.t(), new_state :: Frame.t()}
@optional_callbacks title: 0, uri: 0, uri_template: 0, description: 0
defimpl JSON.Encoder, for: __MODULE__ do
alias Backplane.McpProtocol.Server.Component.Resource
def encode(%Resource{uri_template: uri_template} = resource, _) when not is_nil(uri_template) do
%{
uriTemplate: uri_template,
name: resource.name
}
|> then(&if resource.title, do: Map.put(&1, :title, resource.title), else: &1)
|> then(&if resource.description, do: Map.put(&1, :description, resource.description), else: &1)
|> then(&if resource.mime_type, do: Map.put(&1, :mimeType, resource.mime_type), else: &1)
|> JSON.encode!()
end
def encode(%Resource{} = resource, _) do
resource
|> Map.take([:name, :uri, :description, :title])
|> Map.put(:mimeType, resource.mime_type)
|> JSON.encode!()
end
end
end