Packages
Model Context Protocol (MCP) implementation in Elixir with Phoenix integration
Retired package: Release invalid - wrong usage of Mix.env on runtime function prevents application to start
Current section
Files
Jump to
Current section
Files
lib/mix/interactive/ui.ex
defmodule Mix.Interactive.UI do
@moduledoc """
Common UI elements and formatting functions for interactive MCP shells.
This module provides consistent UI components and formatting helpers for
the interactive CLI interfaces used by the Hermes MCP mix tasks.
It includes functions for displaying colored text, formatted headers,
pretty-printing of data structures, and consistent output formatting.
"""
alias Hermes.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)
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 = Map.get(schema, "type", "any")
description = Map.get(schema, "description", "")
IO.puts(" #{@colors.command}#{name}#{@colors.reset}#{req_marker}: #{type}")
if description != "", do: IO.puts(" #{description}")
end
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
end