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