Current section

Files

Jump to
hermes_mcp lib mix interactive commands.ex
Raw

lib/mix/interactive/commands.ex

defmodule Mix.Interactive.Commands do
@moduledoc """
Common command implementations for interactive MCP shells.
This module contains the implementation of all commands available in the
interactive MCP shells. It provides a consistent set of commands across
different transport implementations, with shared functionality for:
- Listing available commands
- Fetching and displaying tools, prompts, and resources
- Calling tools and getting prompts
- Handling user input and error cases
- Formatting and displaying results
Each command follows a similar pattern, receiving client and loop function
references to enable proper continuation of the interactive shell.
"""
alias Hermes.Client
alias Hermes.MCP.Response
alias Hermes.Transport.SSE
alias Hermes.Transport.STDIO
alias Mix.Interactive
alias Mix.Interactive.State
alias Mix.Interactive.UI
@commands %{
"help" => "Show this help message",
"list_tools" => "List server tools",
"call_tool" => "Call a server tool with arguments",
"list_prompts" => "List server prompts",
"get_prompt" => "Get a server prompt",
"list_resources" => "List server resources",
"read_resource" => "Read a server resource",
"initialize" => "Retry server connection initialization",
"show_state" => "Show internal state of client and transport",
"clear" => "Clear the screen",
"exit" => "Exit the interactive session"
}
@doc """
Returns the map of available commands and their descriptions.
"""
def commands, do: @commands
@doc """
Process a command entered by the user.
"""
def process_command("help", _client, loop_fn), do: print_help(loop_fn)
def process_command("list_tools", client, loop_fn), do: list_tools(client, loop_fn)
def process_command("call_tool", client, loop_fn), do: call_tool(client, loop_fn)
def process_command("list_prompts", client, loop_fn), do: list_prompts(client, loop_fn)
def process_command("get_prompt", client, loop_fn), do: get_prompt(client, loop_fn)
def process_command("initialize", client, loop_fn), do: initialize_client(client, loop_fn)
def process_command("show_state", client, loop_fn), do: show_state(client, loop_fn)
def process_command("list_resources", client, loop_fn) do
list_resources(client, loop_fn)
end
def process_command("read_resource", client, loop_fn) do
read_resource(client, loop_fn)
end
def process_command("clear", _client, loop_fn), do: clear_screen(loop_fn)
def process_command("exit", client, _loop_fn), do: exit_client(client)
def process_command("", _client, loop_fn), do: loop_fn.()
def process_command(unknown, _client, loop_fn), do: unknown_command(unknown, loop_fn)
defp print_help(loop_fn) do
IO.puts("\n#{UI.colors().info}Available commands:#{UI.colors().reset}")
Enum.each(@commands, fn {cmd, desc} ->
IO.puts(" #{UI.colors().command}#{String.pad_trailing(cmd, 15)}#{UI.colors().reset} #{desc}")
end)
IO.puts("")
loop_fn.()
end
defp list_tools(client, loop_fn) do
IO.puts("\n#{UI.colors().info}Fetching tools...#{UI.colors().reset}")
case Client.list_tools(client) do
{:ok, %Response{result: %{"tools" => tools}}} ->
UI.print_items("tools", tools, "name")
{:error, reason} ->
UI.print_error(reason)
end
loop_fn.()
end
defp call_tool(client, loop_fn) do
IO.write("#{UI.colors().prompt}Tool name: #{UI.colors().reset}")
tool_name = "" |> IO.gets() |> String.trim()
IO.write("#{UI.colors().prompt}Tool arguments (JSON): #{UI.colors().reset}")
args_input = "" |> IO.gets() |> String.trim()
case JSON.decode(args_input) do
{:ok, tool_args} ->
perform_tool_call(client, tool_name, tool_args)
{:error, error} ->
IO.puts("#{UI.colors().error}Error parsing JSON: #{inspect(error)}#{UI.colors().reset}")
end
loop_fn.()
end
defp perform_tool_call(client, tool_name, tool_args) do
IO.puts("\n#{UI.colors().info}Calling tool #{tool_name}...#{UI.colors().reset}")
case Client.call_tool(client, tool_name, tool_args) do
{:ok, %Response{result: result}} ->
IO.puts("#{UI.colors().success}Tool call successful#{UI.colors().reset}")
IO.puts("\n#{UI.colors().info}Result:#{UI.colors().reset}")
IO.puts(UI.format_output(result))
{:error, reason} ->
UI.print_error(reason)
end
IO.puts("")
end
defp list_prompts(client, loop_fn) do
IO.puts("\n#{UI.colors().info}Fetching prompts...#{UI.colors().reset}")
case Client.list_prompts(client) do
{:ok, %Response{result: %{"prompts" => prompts}}} ->
UI.print_items("prompts", prompts, "name")
{:error, reason} ->
UI.print_error(reason)
end
loop_fn.()
end
defp get_prompt(client, loop_fn) do
IO.write("#{UI.colors().prompt}Prompt name: #{UI.colors().reset}")
prompt_name = "" |> IO.gets() |> String.trim()
IO.write("#{UI.colors().prompt}Prompt arguments (JSON): #{UI.colors().reset}")
args_input = "" |> IO.gets() |> String.trim()
case JSON.decode(args_input) do
{:ok, prompt_args} ->
perform_get_prompt(client, prompt_name, prompt_args)
{:error, error} ->
IO.puts("#{UI.colors().error}Error parsing JSON: #{inspect(error)}#{UI.colors().reset}")
end
loop_fn.()
end
defp perform_get_prompt(client, prompt_name, prompt_args) do
IO.puts("\n#{UI.colors().info}Getting prompt #{prompt_name}...#{UI.colors().reset}")
case Client.get_prompt(client, prompt_name, prompt_args) do
{:ok, %Response{result: result}} ->
IO.puts("#{UI.colors().success}Got prompt successfully#{UI.colors().reset}")
IO.puts("\n#{UI.colors().info}Result:#{UI.colors().reset}")
IO.puts(UI.format_output(result))
{:error, reason} ->
UI.print_error(reason)
end
IO.puts("")
end
defp list_resources(client, loop_fn) do
IO.puts("\n#{UI.colors().info}Fetching resources...#{UI.colors().reset}")
case Client.list_resources(client) do
{:ok, %Response{result: %{"resources" => resources}}} ->
UI.print_items("resources", resources, "uri")
{:error, reason} ->
UI.print_error(reason)
end
loop_fn.()
end
defp read_resource(client, loop_fn) do
IO.write("#{UI.colors().prompt}Resource URI: #{UI.colors().reset}")
resource_uri = "" |> IO.gets() |> String.trim()
IO.puts("\n#{UI.colors().info}Reading resource #{resource_uri}...#{UI.colors().reset}")
case Client.read_resource(client, resource_uri) do
{:ok, %Response{result: result}} ->
IO.puts("#{UI.colors().success}Read resource successfully#{UI.colors().reset}")
IO.puts("\n#{UI.colors().info}Content:#{UI.colors().reset}")
IO.puts(UI.format_output(result))
{:error, reason} ->
UI.print_error(reason)
end
IO.puts("")
loop_fn.()
end
defp clear_screen(loop_fn) do
IO.write(IO.ANSI.clear() <> IO.ANSI.home())
loop_fn.()
end
defp exit_client(client) do
IO.puts("\n#{UI.colors().info}Closing connection and exiting...#{UI.colors().reset}")
Client.close(client)
:ok
end
defp initialize_client(client, loop_fn) do
IO.puts("\n#{UI.colors().info}Reinitializing client connection...#{UI.colors().reset}")
old_state = :sys.get_state(client)
GenServer.cast(client, :initialize)
Process.flag(:trap_exit, true)
:timer.sleep(500)
receive do
{:EXIT, _, {:error, err}} ->
print_initialization_error(err, old_state)
loop_fn.()
after
500 -> :ok
end
if Process.alive?(client) do
Interactive.CLI.check_client_connection(client)
loop_fn.()
else
IO.puts("#{UI.colors().error}Client #{inspect(client)} is not alive#{UI.colors().reset}")
if old_state do
IO.puts("#{UI.colors().info}Last client state before failure:#{UI.colors().reset}")
State.print_state(client)
end
loop_fn.()
end
end
defp print_initialization_error(error, state) do
UI.print_error(error)
verbose = System.get_env("HERMES_VERBOSE") == "1"
if verbose && state do
IO.puts("\n#{UI.colors().info}Additional error context (HERMES_VERBOSE=1):#{UI.colors().reset}")
case error do
%{reason: :connection_refused} ->
print_connection_error_context(state)
%{reason: :request_timeout} ->
print_timeout_error_context(state)
%{reason: :server_error, data: data} ->
print_server_error_context(data, state)
_ ->
IO.puts(" #{UI.colors().info}Last client state:#{UI.colors().reset}")
# Can't use print_state directly as client might be dead
IO.puts(" #{inspect(state, pretty: true, limit: 10)}")
end
else
IO.puts("#{UI.colors().info}For more detailed error information, set HERMES_VERBOSE=1#{UI.colors().reset}")
end
end
defp print_connection_error_context(state) do
transport_info = state.transport
case transport_info do
%{layer: SSE} ->
transport_pid = transport_info[:name] || SSE
if Process.alive?(transport_pid) do
transport_state = :sys.get_state(transport_pid)
IO.puts(" #{UI.colors().info}Server URL:#{UI.colors().reset} #{transport_state[:server_url]}")
IO.puts(" #{UI.colors().info}SSE URL:#{UI.colors().reset} #{transport_state[:sse_url]}")
end
%{layer: STDIO} ->
transport_pid = transport_info[:name] || STDIO
if Process.alive?(transport_pid) do
transport_state = :sys.get_state(transport_pid)
IO.puts(" #{UI.colors().info}Command:#{UI.colors().reset} #{transport_state.command}")
print_stdio_args(transport_state)
end
_ ->
IO.puts(" #{UI.colors().info}Transport:#{UI.colors().reset} #{inspect(transport_info)}")
end
IO.puts(" #{UI.colors().info}Client Info:#{UI.colors().reset} #{inspect(state.client_info)}")
end
defp print_timeout_error_context(state) do
IO.puts(" #{UI.colors().info}Protocol Version:#{UI.colors().reset} #{state.protocol_version}")
IO.puts(" #{UI.colors().info}Pending Requests:#{UI.colors().reset} #{map_size(state.pending_requests)}")
if state.server_capabilities do
IO.puts(" #{UI.colors().info}Server Capabilities:#{UI.colors().reset} #{inspect(state.server_capabilities)}")
end
end
defp print_server_error_context(data, state) do
IO.puts(" #{UI.colors().info}Server Error Data:#{UI.colors().reset} #{inspect(data)}")
IO.puts(" #{UI.colors().info}Protocol Version:#{UI.colors().reset} #{state.protocol_version}")
if state.server_info do
IO.puts(" #{UI.colors().info}Server Info:#{UI.colors().reset} #{inspect(state.server_info)}")
end
end
defp show_state(client, loop_fn) do
IO.puts("\n#{UI.colors().info}Getting internal state information...#{UI.colors().reset}")
State.print_state(client)
IO.puts("")
loop_fn.()
end
defp unknown_command(command, loop_fn) do
IO.puts("#{UI.colors().error}Unknown command: #{command}#{UI.colors().reset}")
IO.puts("Type #{UI.colors().command}help#{UI.colors().reset} for available commands")
loop_fn.()
end
defp print_stdio_args(state) do
if state.args do
IO.puts(" #{UI.colors().info}Args:#{UI.colors().reset} #{inspect(state.args)}")
end
end
end