Normandy.MCP.ToolWrapper
(normandy v2.2.0)
View Source
Wraps an MCP tool as a Normandy.Tool.
This allows MCP tools discovered via any Claudio.MCP.Client adapter
to be registered in a Normandy.Tools.Toolset 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.new(Claudio.MCP.Adapters.ExMCP, mcp_client_pid, mcp_tool, prefix: "web")
# The model sees the tool as "web__search".
agent = BaseAgent.register_tool(agent, wrapper)Results
The server's CallToolResult reaches the model as text: its text blocks joined by
newlines, and a one-line [<type> content omitted] note for each image, audio or
resource block. A result with isError: true is an error result (is_error on the
tool_result). A result in no known shape (no content list) is passed on as it is.
Tool names and policy rules
The server chooses its tools' names, so a name says nothing about what a tool does. A
Normandy.Behaviours.PolicyEngine.Ruleset rule matches the name only: a server that
names a destructive tool read_and_purge matches the rule "read_*". That is why
:prefix is required. The model sees prefix <> "__" <> name, so the rule "web__*"
matches exactly the tools of the server you gave the prefix "web" (and any local tool
you chose to name web__...).
A prefix is letters, digits, - and single _ (no __, no _ at either end), at
most 61 bytes, so the first __ in a name always ends the prefix. The whole name must
still be a valid tool name (^[a-zA-Z0-9_-]{1,64}$): with a prefix, a server's tool
name can be at most 62 - byte_size(prefix) bytes, and a server with a longer or
dotted name fails registration ({:error, {:invalid_tool_name, name}}).
prefix: false keeps the server's own names. Use it only for a server you control, and
write no name-glob rule such a server could match.
Trust
An MCP server is outside your trust boundary. Its tool descriptions and input schemas
are sent to the model on every request, and its results become the model's input: each
is a prompt-injection channel that Normandy passes through unchanged. Connect servers
you trust as much as your own tools' code, and gate tools that change state with a
policy (:require_approval) rather than trusting a server's names or descriptions.
Trace context
Unlike Normandy.A2A.AgentTool, an MCP call does not carry the W3C trace
context to the server. Claudio.MCP.Client.call_tool/4 takes the tool name,
its arguments and transport options (a timeout), with no request metadata
(MCP _meta) or header field, and the adapters build the request inside the
MCP client library. Putting traceparent into the arguments would hand the
tool an input it did not declare. The MCP server's spans therefore start a
trace of their own.
Summary
Functions
Creates a new ToolWrapper for an MCP tool.
Types
@type t() :: %Normandy.MCP.ToolWrapper{ adapter: module(), client: term(), prefix: String.t() | false, tool: Claudio.MCP.Client.Tool.t() }
Functions
@spec new(module(), term(), Claudio.MCP.Client.Tool.t(), keyword()) :: t()
Creates a new ToolWrapper for an MCP tool.
Parameters
adapter- The MCP adapter module (e.g.,Claudio.MCP.Adapters.ExMCP)client- The MCP client connectiontool- AClaudio.MCP.Client.Toolstructopts- see Options
Options
:prefix(String.t/0orfalse) - Required. The server's namespace: the model sees each tool asprefix__name. Letters, digits,-and single_(no__, no_at either end), at most 61 bytes.falsekeeps the server's own names (see "Tool names and policy rules").