Packages
fnord
0.9.36
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, registered in the current process tree"
def start_link(_opts \\ []) do
with {:ok, pid} <- Agent.start_link(fn -> %{} end) do
Services.Instance.register(__MODULE__, pid)
{:ok, pid}
end
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
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}"
# The modules are VM-global; the ensure_loaded? guard makes
# re-registration idempotent when another tree already defined them.
# Concurrent first-definition is serialized by the code server; worst
# case is a redefinition warning, not corruption.
unless Code.ensure_loaded?(mod) do
Module.create(mod, module_ast(spec_data, server, name), __ENV__)
end
# The tool_name => module index is tree-scoped, so it must be written
# even when the module itself was defined by an earlier tree.
Agent.update(instance(), &Map.put(&1, tool_name, mod))
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. Emits the
# flat Responses-API shape: %{type, name, description, parameters} at the
# top level. AI.Tools.Params.normalize_spec/1 (the central validator)
# expects this exact shape - nested chat-completions-style
# %{function: %{...}} would fail loudly there. Keep this in sync if the
# MCP `inputSchema` ever grows additional wrapper layers.
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",
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, :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)
MCP.Client.call_tool(@server, @tool, args, timeout: timeout)
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
Agent.get(instance(), & &1)
end
defp instance(), do: Services.Instance.fetch!(__MODULE__)
end