defmodule Mix.Interactive.UI do @moduledoc false alias Backplane.McpProtocol.MCP.Error alias IO.ANSI @colors %{ prompt: ANSI.bright() <> ANSI.cyan(), command: ANSI.green(), error: ANSI.red(), success: ANSI.bright() <> ANSI.green(), info: ANSI.yellow(), warning: ANSI.bright() <> ANSI.yellow(), reset: ANSI.reset() } @doc """ Returns the color configuration map for styling output. """ def colors, do: @colors @doc """ Creates a styled header for an interactive session. """ def header(title) do """ #{ANSI.bright()}#{ANSI.cyan()} ┌─────────────────────────────────────────┐ │ #{String.pad_trailing(title, 34)}│ └─────────────────────────────────────────┘ #{ANSI.reset()} """ end @doc """ Formats JSON or other data for pretty-printing with indentation. """ def format_output(data) do data |> JSON.encode!() |> String.split("\n") |> Enum.map_join("\n", fn line -> " " <> line end) rescue _ -> " " <> inspect(data, pretty: true, width: 80) end @doc """ Formats error messages with appropriate styling. """ def print_error(reason) do message = format_error_message(reason) IO.puts("#{@colors.error}Error: #{message}#{@colors.reset}") end defp format_error_message(%Error{reason: :server_capabilities_not_set}) do "Server capabilities not available. Connection may not be established." end defp format_error_message(%Error{reason: :connection_refused}) do "Connection refused. Server may be unavailable." end defp format_error_message(%Error{reason: :request_timeout}) do "Request timed out. Server may be busy or unreachable." end defp format_error_message(%Error{reason: reason, data: data}) do "#{reason} #{inspect(data, pretty: true)}" end defp format_error_message(other) do inspect(other, pretty: true) end @doc """ Formats lists of tools/prompts/resources with helpful styling. """ def print_items(title, [_ | _] = items, key_field) do IO.puts("#{@colors.success}Found #{length(items)} #{title}#{@colors.reset}") IO.puts("\n#{@colors.info}Available #{title}:#{@colors.reset}") Enum.each(items, fn item -> IO.puts(" #{@colors.command}#{item[key_field]}#{@colors.reset}") if Map.has_key?(item, "description"), do: IO.puts(" #{item["description"]}") cond do title == "tools" && Map.has_key?(item, "inputSchema") -> print_schema(item["inputSchema"]) title == "prompts" && Map.has_key?(item, "arguments") -> print_prompt_arguments(item["arguments"]) title == "resources" -> print_resource_details(item) title == "resource templates" -> print_resource_template_details(item) true -> nil end end) IO.puts("") end def print_items(title, items, _key_field) do IO.puts("#{@colors.success}Found #{length(items)} #{title}#{@colors.reset}") IO.puts("") end defp print_schema(schema) when is_map(schema) do if schema["type"] == "object" && Map.has_key?(schema, "properties") do IO.puts(" #{@colors.info}Arguments:#{@colors.reset}") print_properties(schema["properties"], Map.get(schema, "required", [])) end end defp print_schema(_), do: nil defp print_properties(properties, required) when is_map(properties) do Enum.each(properties, fn {prop_name, prop_schema} -> print_property(prop_name, prop_schema, prop_name in required) end) end defp print_property(name, schema, required) do req_marker = if required, do: " (required)", else: "" type = schema |> Map.get("type", "any") |> format_schema_type() description = Map.get(schema, "description", "") IO.puts(" #{@colors.command}#{name}#{@colors.reset}#{req_marker}: #{type}") if description != "", do: IO.puts(" #{description}") end defp format_schema_type(types) when is_list(types), do: Enum.join(types, " | ") defp format_schema_type(type), do: type defp print_prompt_arguments(arguments) when is_list(arguments) do IO.puts(" #{@colors.info}Arguments:#{@colors.reset}") Enum.each(arguments, fn arg -> req_marker = if Map.get(arg, "required", false), do: " (required)", else: "" name = Map.get(arg, "name", "") description = Map.get(arg, "description", "") IO.puts(" #{@colors.command}#{name}#{@colors.reset}#{req_marker}") if description != "", do: IO.puts(" #{description}") end) end defp print_prompt_arguments(_), do: nil defp print_resource_details(resource) when is_map(resource) do if Map.has_key?(resource, "mimeType") do IO.puts(" #{@colors.info}Type:#{@colors.reset} #{resource["mimeType"]}") end if Map.has_key?(resource, "name") && resource["name"] != resource["uri"] do IO.puts(" #{@colors.info}Name:#{@colors.reset} #{resource["name"]}") end end defp print_resource_template_details(template) when is_map(template) do if Map.has_key?(template, "uriTemplate") do IO.puts(" #{@colors.info}URI Template:#{@colors.reset} #{template["uriTemplate"]}") end if Map.has_key?(template, "mimeType") do IO.puts(" #{@colors.info}Type:#{@colors.reset} #{template["mimeType"]}") end # Show title if it differs from the name if Map.has_key?(template, "title") && template["title"] != template["name"] do IO.puts(" #{@colors.info}Title:#{@colors.reset} #{template["title"]}") end end end