Packages
Model Context Protocol (MCP) implementation in Elixir with Phoenix integration
Current section
Files
Jump to
Current section
Files
lib/backplane/mcp_protocol/server/handlers/resources.ex
defmodule Backplane.McpProtocol.Server.Handlers.Resources do
@moduledoc false
alias Backplane.McpProtocol.MCP.Error
alias Backplane.McpProtocol.Server.Component.Resource
alias Backplane.McpProtocol.Server.Component.URITemplate
alias Backplane.McpProtocol.Server.Frame
alias Backplane.McpProtocol.Server.Handlers
alias Backplane.McpProtocol.Server.Response
@spec handle_list(map, Frame.t(), module()) ::
{:reply, map(), Frame.t()} | {:error, Error.t(), Frame.t()}
def handle_list(request, frame, server_module) do
resources =
server_module
|> Handlers.get_server_resources(frame)
|> Enum.filter(&visible?(&1, frame))
limit = frame.pagination_limit
{resources, cursor} = Handlers.maybe_paginate(request, resources, limit)
{:reply,
then(
%{"resources" => resources},
&if(cursor, do: Map.put(&1, "nextCursor", cursor), else: &1)
), frame}
end
@spec handle_templates_list(map, Frame.t(), module()) ::
{:reply, map(), Frame.t()} | {:error, Error.t(), Frame.t()}
def handle_templates_list(request, frame, server_module) do
templates =
server_module
|> Handlers.get_server_resource_templates(frame)
|> Enum.filter(&visible?(&1, frame))
limit = frame.pagination_limit
{templates, cursor} = Handlers.maybe_paginate(request, templates, limit)
{:reply,
then(
%{"resourceTemplates" => templates},
&if(cursor, do: Map.put(&1, "nextCursor", cursor), else: &1)
), frame}
end
@spec handle_read(map(), Frame.t(), module()) ::
{:reply, map(), Frame.t()} | {:error, Error.t(), Frame.t()}
def handle_read(%{"params" => %{"uri" => uri}}, frame, server) when is_binary(uri) do
resources = Handlers.get_server_resources(server, frame)
templates = Handlers.get_server_resource_templates(server, frame)
case find_static_resource(resources, uri) do
%Resource{} = resource ->
with :ok <- check_scopes(resource, frame) do
read_single_resource(server, resource, uri, frame)
end
nil ->
try_resource_templates(templates, server, uri, frame)
end
end
@spec handle_subscribe(map(), Frame.t(), module()) ::
{:reply, map(), Frame.t()} | {:error, Error.t(), Frame.t()}
def handle_subscribe(%{"params" => %{"uri" => uri}}, frame, server) when is_binary(uri) do
if subscribe_enabled?(server) do
with :ok <- check_scopes_for_uri(server, uri, frame) do
{:reply, %{}, Frame.subscribe_resource(frame, uri)}
end
else
{:error, Error.protocol(:method_not_found, %{method: "resources/subscribe"}), frame}
end
end
@spec handle_unsubscribe(map(), Frame.t(), module()) ::
{:reply, map(), Frame.t()} | {:error, Error.t(), Frame.t()}
def handle_unsubscribe(%{"params" => %{"uri" => uri}}, frame, server) when is_binary(uri) do
if subscribe_enabled?(server) do
{:reply, %{}, Frame.unsubscribe_resource(frame, uri)}
else
{:error, Error.protocol(:method_not_found, %{method: "resources/unsubscribe"}), frame}
end
end
# Private functions
defp check_scopes(%Resource{scopes: []}, _frame), do: :ok
defp check_scopes(%Resource{scopes: required}, frame) do
granted = Frame.scopes(frame)
missing = Enum.reject(required, &(&1 in granted))
if missing == [] do
:ok
else
{:error, Error.execution("insufficient_scope", %{required: required, granted: granted}), frame}
end
end
defp visible?(%Resource{scopes: []}, _frame), do: true
defp visible?(%Resource{scopes: required}, frame), do: Frame.has_all_scopes?(frame, required)
defp check_scopes_for_uri(server, uri, frame) do
resources = Handlers.get_server_resources(server, frame)
case find_static_resource(resources, uri) do
%Resource{} = resource ->
check_scopes(resource, frame)
nil ->
templates = Handlers.get_server_resource_templates(server, frame)
case find_matching_template(templates, uri) do
%Resource{} = template -> check_scopes(template, frame)
nil -> :ok
end
end
end
defp find_matching_template(templates, uri) do
Enum.find(templates, fn template ->
match?({:ok, _}, URITemplate.match(template.uri_template, uri))
end)
end
defp subscribe_enabled?(server) do
get_in(server.server_capabilities(), ["resources", :subscribe]) == true
end
defp find_static_resource(resources, uri), do: Enum.find(resources, &(&1.uri == uri))
defp try_resource_templates(templates, server, uri, frame, pending_scope_error \\ nil)
defp try_resource_templates([], _server, _uri, _frame, {:error, %Error{}, _} = pending) do
pending
end
defp try_resource_templates([], _server, uri, frame, nil) do
payload = %{message: "Resource not found: #{uri}"}
error = Error.resource(:not_found, payload)
{:error, error, frame}
end
defp try_resource_templates([template | rest], server, uri, frame, pending_scope_error) do
case URITemplate.match(template.uri_template, uri) do
{:ok, vars} -> try_matching_template(template, vars, rest, server, uri, frame, pending_scope_error)
:error -> try_resource_templates(rest, server, uri, frame, pending_scope_error)
end
end
defp try_matching_template(template, vars, rest, server, uri, frame, pending_scope_error) do
case check_scopes(template, frame) do
:ok ->
try_read_with_fallback(template, vars, rest, server, uri, frame, pending_scope_error)
{:error, _, _} = scope_error ->
try_resource_templates(rest, server, uri, frame, pending_scope_error || scope_error)
end
end
defp try_read_with_fallback(template, vars, rest, server, uri, frame, pending_scope_error) do
case read_single_resource(server, template, uri, frame, vars) do
{:error, %Error{reason: :resource_not_found}, _frame} ->
try_resource_templates(rest, server, uri, frame, pending_scope_error)
other ->
other
end
end
defp read_single_resource(server, resource, uri, frame, vars \\ %{})
defp read_single_resource(server, %Resource{handler: nil, mime_type: mime_type}, uri, frame, _vars) do
case server.handle_resource_read(uri, frame) do
{:reply, %Response{} = response, frame} ->
content = Response.to_protocol(response, uri, mime_type)
{:reply, %{"contents" => [content]}, frame}
{:noreply, frame} ->
content = %{"uri" => uri, "mimeType" => mime_type, "text" => ""}
{:reply, %{"contents" => [content]}, frame}
{:error, %Error{} = error, frame} ->
{:error, error, frame}
end
end
defp read_single_resource(_server, %Resource{handler: handler, mime_type: mime_type}, uri, frame, vars) do
case handler.read(%{"uri" => uri, "params" => vars}, frame) do
{:reply, %Response{} = response, frame} ->
content = Response.to_protocol(response, uri, mime_type)
{:reply, %{"contents" => [content]}, frame}
{:noreply, frame} ->
content = %{"uri" => uri, "mimeType" => mime_type, "text" => ""}
{:reply, %{"contents" => [content]}, frame}
{:error, %Error{} = error, frame} ->
{:error, error, frame}
end
end
end