defmodule Backplane.McpProtocol.Server.Handlers do @moduledoc false alias Backplane.McpProtocol.MCP.Error alias Backplane.McpProtocol.Server.Frame alias Backplane.McpProtocol.Server.Handlers.Completion alias Backplane.McpProtocol.Server.Handlers.Prompts alias Backplane.McpProtocol.Server.Handlers.Resources alias Backplane.McpProtocol.Server.Handlers.Tools alias Backplane.McpProtocol.Server.Response @spec handle(map, module, Frame.t()) :: {:reply, response :: Response.t(), new_state :: Frame.t()} | {:noreply, new_state :: Frame.t()} | {:error, error :: Error.t(), new_state :: Frame.t()} def handle(%{"method" => "tools/" <> action} = request, module, frame) do case action do "list" -> Tools.handle_list(request, frame, module) "call" -> Tools.handle_call(request, frame, module) _ -> {:error, Error.protocol(:method_not_found, %{method: request["method"]}), frame} end end def handle(%{"method" => "prompts/" <> action} = request, module, frame) do case action do "list" -> Prompts.handle_list(request, frame, module) "get" -> Prompts.handle_get(request, frame, module) _ -> {:error, Error.protocol(:method_not_found, %{method: request["method"]}), frame} end end def handle(%{"method" => "resources/" <> action} = request, module, frame) do case action do "list" -> Resources.handle_list(request, frame, module) "read" -> Resources.handle_read(request, frame, module) "templates/list" -> Resources.handle_templates_list(request, frame, module) "subscribe" -> Resources.handle_subscribe(request, frame, module) "unsubscribe" -> Resources.handle_unsubscribe(request, frame, module) _ -> {:error, Error.protocol(:method_not_found, %{method: request["method"]}), frame} end end def handle(%{"method" => "completion/complete"} = request, module, frame) do Completion.handle_complete(request, frame, module) end def handle(%{"method" => method}, _module, frame) do {:error, Error.protocol(:method_not_found, %{method: method}), frame} end def get_server_tools(module, frame) do Enum.sort_by(module.__components__(:tool) ++ Frame.get_tools(frame), & &1.name) end def get_server_prompts(module, frame) do Enum.sort_by(module.__components__(:prompt) ++ Frame.get_prompts(frame), & &1.name) end def get_server_resources(module, frame) do compile_time = :resource |> module.__components__() |> Enum.reject(& &1.uri_template) runtime = Map.values(frame.resources) Enum.sort_by(compile_time ++ runtime, & &1.name) end def get_server_resource_templates(module, frame) do compile_time = :resource |> module.__components__() |> Enum.filter(& &1.uri_template) runtime = Map.values(frame.resource_templates) Enum.sort_by(compile_time ++ runtime, & &1.name) end @spec maybe_paginate(map, list(struct), non_neg_integer | nil) :: {list(struct), cursor :: String.t() | nil} def maybe_paginate(_, components, nil), do: {components, nil} def maybe_paginate(%{"params" => %{"cursor" => cursor}}, components, limit) when limit > 0 do last_name = Base.decode64!(cursor, padding: false) {_taken, remain} = Enum.split_while(components, &(&1.name <= last_name)) maybe_paginate(%{}, remain, limit) end def maybe_paginate(_, components, limit) when limit > 0 do if length(components) > limit do taken = Enum.take(components, limit) next_name = List.last(taken).name remain = length(components -- taken) {taken, if(next_name && remain > 0, do: Base.encode64(next_name, padding: false))} else {components, nil} end end end