Packages
Elixir framework for building production-ready AI agents and LLM applications: type-safe schemas with JSON Schema generation, tool calling, streaming, multi-agent coordination, guardrails, and distributed fault-tolerant sessions, with first-class Anthropic Claude support.
Current section
Files
Jump to
Current section
Files
lib/normandy/mcp/tool_wrapper.ex
defmodule Normandy.MCP.ToolWrapper do
@moduledoc """
Wraps an MCP tool as a Normandy `BaseTool` implementation.
This allows MCP tools discovered via any `Claudio.MCP.Client` adapter
to be registered in a Normandy tool registry and used in the agent's
tool execution loop.
## Example
alias Claudio.MCP.Client.Tool
alias Normandy.MCP.ToolWrapper
mcp_tool = %Tool{
name: "search",
description: "Search the web",
input_schema: %{"type" => "object", "properties" => %{"query" => %{"type" => "string"}}}
}
wrapper = %ToolWrapper{
adapter: Claudio.MCP.Adapters.ExMCP,
client: mcp_client_pid,
tool: mcp_tool,
prefix: "my_server"
}
# Register in agent
agent = BaseAgent.register_tool(agent, wrapper)
"""
@type t :: %__MODULE__{
adapter: module(),
client: term(),
tool: Claudio.MCP.Client.Tool.t(),
prefix: String.t() | nil,
input: map()
}
defstruct [:adapter, :client, :tool, :prefix, input: %{}]
@doc """
Creates a new ToolWrapper for an MCP tool.
## Parameters
- `adapter` - The MCP adapter module (e.g., `Claudio.MCP.Adapters.ExMCP`)
- `client` - The MCP client connection
- `tool` - A `Claudio.MCP.Client.Tool` struct
- `opts` - Options including `:prefix` for namespacing
"""
@spec new(module(), term(), Claudio.MCP.Client.Tool.t(), keyword()) :: t()
def new(adapter, client, tool, opts \\ []) do
%__MODULE__{
adapter: adapter,
client: client,
tool: tool,
prefix: Keyword.get(opts, :prefix)
}
end
@doc """
Prepares the tool with LLM-provided input parameters.
Called by the BaseAgent tool loop instead of `struct/2` for tools
with dynamic input schemas.
"""
@spec prepare_input(t(), map()) :: t()
def prepare_input(%__MODULE__{} = wrapper, input) when is_map(input) do
%{wrapper | input: input}
end
defimpl Normandy.Tools.BaseTool do
def tool_name(%{tool: tool, prefix: nil}), do: tool.name
def tool_name(%{tool: tool, prefix: prefix}) do
"#{prefix}__#{tool.name}"
end
def tool_description(%{tool: tool}) do
tool.description || "MCP tool: #{tool.name}"
end
def input_schema(%{tool: tool}) do
tool.input_schema
end
def run(%{adapter: adapter, client: client, tool: tool, input: input}) do
case adapter.call_tool(client, tool.name, input, []) do
{:ok, result} ->
{:ok, result}
{:error, reason} ->
{:error, "MCP tool '#{tool.name}' failed: #{inspect(reason)}"}
end
end
end
end