Packages
fnord
0.9.40
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
@definition_tab :mcp_tool_definitions
@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)
Services.Globals.ensure_shared_table(@definition_tab, [
:named_table,
:public,
:set,
read_concurrency: true
])
{: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; serialize first-definition across the VM
# to avoid duplicate Module.create/3 races under concurrent discovery.
ensure_tool_module_defined(mod, spec_data, server, tool_name)
# 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
# Generated tool modules are VM-global, while the Agent state is tree-scoped.
# The shared ETS table serializes first-definition and gives waiters a ready
# signal before they publish the tree-local tool_name => module mapping.
defp ensure_tool_module_defined(mod, spec_data, server, tool_name) do
cond do
Code.ensure_loaded?(mod) ->
:ok
claim_tool_definition(mod) == :owner ->
define_tool_module(mod, spec_data, server, tool_name)
true ->
wait_for_tool_module(mod, spec_data, server, tool_name)
end
end
defp claim_tool_definition(mod) do
if :ets.insert_new(@definition_tab, {mod, :defining}) do
:owner
else
:wait
end
end
defp define_tool_module(mod, spec_data, server, tool_name) do
try do
Module.create(mod, module_ast(spec_data, server, tool_name), __ENV__)
:ets.insert(@definition_tab, {mod, :ready})
:ok
rescue
error ->
:ets.delete(@definition_tab, mod)
reraise error, __STACKTRACE__
catch
kind, reason ->
:ets.delete(@definition_tab, mod)
:erlang.raise(kind, reason, __STACKTRACE__)
end
end
defp wait_for_tool_module(mod, spec_data, server, tool_name) do
cond do
Code.ensure_loaded?(mod) ->
:ok
:ets.lookup(@definition_tab, mod) == [{mod, :ready}] ->
:ok
:ets.lookup(@definition_tab, mod) == [{mod, :defining}] ->
Process.sleep(10)
wait_for_tool_module(mod, spec_data, server, tool_name)
true ->
ensure_tool_module_defined(mod, spec_data, server, tool_name)
end
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