Packages
fnord
0.9.25
0.9.40
0.9.39
0.9.38
0.9.37
0.9.36
0.9.35
0.9.34
0.9.33
0.9.32
0.9.31
0.9.30
0.9.29
0.9.28
0.9.27
0.9.26
0.9.25
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.99
0.8.98
0.8.97
0.8.96
0.8.95
0.8.94
0.8.93
0.8.92
0.8.91
0.8.90
0.8.89
0.8.88
0.8.87
0.8.86
0.8.85
0.8.84
0.8.83
0.8.82
0.8.81
0.8.80
0.8.79
0.8.78
0.8.77
0.8.76
0.8.75
0.8.74
0.8.73
0.8.72
0.8.71
0.8.70
0.8.69
0.8.68
0.8.67
0.8.66
0.8.65
0.8.64
0.8.63
0.8.62
0.8.61
0.8.60
0.8.59
0.8.58
0.8.57
0.8.56
0.8.55
0.8.54
0.8.53
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.47
0.8.46
0.8.45
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.1
0.8.0
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
AI code archaeology
Current section
Files
Jump to
Current section
Files
lib/mcp/tools.ex
defmodule MCP.Tools do
@moduledoc false
use Agent
@doc "Start the MCP.Tools agent"
def start_link(_opts \\ []) do
Agent.start_link(fn -> %{} end, name: __MODULE__)
end
@doc "Register or define AI.Tools modules for each MCP tool"
@spec register_server_tools(String.t(), [map()]) :: :ok
def register_server_tools(server, tools) when is_binary(server) and is_list(tools) do
ensure_started()
Enum.each(tools, fn tool_spec ->
name = tool_spec["name"]
mod = module_name(server, name)
spec_data = default_spec(server, tool_spec)
tool_name = "#{server}_#{name}"
unless Code.ensure_loaded?(mod) do
Module.create(mod, module_ast(spec_data, server, name), __ENV__)
# Track the module for later retrieval
Agent.update(__MODULE__, &Map.put(&1, tool_name, mod))
end
end)
:ok
end
# Build the module name under AI.Tools.MCP.<ServerCamel>.<ToolCamel>
defp module_name(server, tool_name) do
server_mod = Macro.camelize(server)
tool_mod = Macro.camelize(tool_name)
Module.concat([AI.Tools.MCP, server_mod, tool_mod])
end
# Construct default spec metadata for the dynamic tool module
defp default_spec(server, tool_spec) do
input_schema = Map.get(tool_spec, "inputSchema", %{})
# Ensure the parameters have a required field (even if empty)
parameters = Map.put_new(input_schema, "required", [])
desc = """
#{Map.get(tool_spec, "description", "")}
Note: This tool is provided by the MCP server, "#{server}".
"""
%{
type: "function",
function: %{
name: "#{server}_#{tool_spec["name"]}",
description: String.trim(desc),
parameters: parameters
}
}
end
# Generate AST for the dynamic tool module implementing AI.Tools callbacks
defp module_ast(spec_data, server, tool_name) do
quote do
@moduledoc false
@behaviour AI.Tools
@compile {:no_warn_undefined, __MODULE__}
@server unquote(server)
@tool unquote(tool_name)
@spec_data unquote(Macro.escape(spec_data))
@impl true
@impl true
def async?, do: true
@impl true
def is_available?, do: true
@impl true
def spec, do: @spec_data
@impl true
def read_args(args), do: {:ok, args}
@impl true
def ui_note_on_request(args) do
tool_name = Map.get(@spec_data.function, :name, @tool)
case Map.keys(args) do
[] -> "Calling #{tool_name}"
keys -> "Calling #{tool_name} with #{Enum.join(keys, ", ")}"
end
end
@impl true
def ui_note_on_result(_args, result) do
case result do
{:ok, data} when is_map(data) ->
case Map.get(data, "content") do
[%{"text" => text}] when is_binary(text) ->
if String.length(text) > 100 do
{"Result", String.slice(text, 0, 97) <> "..."}
else
{"Result", text}
end
content when is_list(content) ->
{"Result", "#{length(content)} items"}
_ ->
{"Result", "Success"}
end
{:ok, text} when is_binary(text) ->
if String.length(text) > 100 do
{"Result", String.slice(text, 0, 97) <> "..."}
else
{"Result", text}
end
{:error, reason} ->
{"Error", inspect(reason)}
_ ->
nil
end
end
@impl true
def tool_call_failure_message(_args, _reason), do: :default
@impl true
def call(args) do
timeout = min(Map.get(@spec_data, "timeout_ms", 30_000), 300_000)
client = MCP.Supervisor.instance_name(@server)
# Call the Base GenServer directly by its registered name
try do
case Hermes.Client.Base.call_tool(client, @tool, args, timeout: timeout) do
{:ok, %Hermes.MCP.Response{result: result}} -> {:ok, result}
{:ok, res} -> {:ok, res}
{:error, reason} -> {:error, reason}
end
catch
:exit, _ -> {:error, "MCP client not available"}
end
end
end
end
@doc "Get a map of tool name to module for all registered MCP tools"
@spec module_map() :: %{String.t() => module()}
def module_map do
ensure_started()
Agent.get(__MODULE__, & &1)
end
# Ensure the agent is started
defp ensure_started do
case Process.whereis(__MODULE__) do
nil -> start_link([])
_ -> :ok
end
end
end