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

t()

@type t() :: %Normandy.MCP.ToolWrapper{
  adapter: module(),
  client: term(),
  prefix: String.t() | false,
  tool: Claudio.MCP.Client.Tool.t()
}

Functions

new(adapter, client, tool, opts \\ [])

@spec new(module(), term(), Claudio.MCP.Client.Tool.t(), keyword()) :: t()

Creates a new ToolWrapper for an MCP tool.

Parameters

Options

  • :prefix (String.t/0 or false) - Required. The server's namespace: the model sees each tool as prefix__name. Letters, digits, - and single _ (no __, no _ at either end), at most 61 bytes. false keeps the server's own names (see "Tool names and policy rules").