Defines the behaviour for implementing MCP tools in GenMCP.Suite.
Tools are the primary mechanism for clients to execute operations on the server. Each tool implementation provides metadata, input validation, execution logic, and optional asynchronous continuation handling.
Implementation Patterns
Use use GenMCP.Suite.Tool with options to auto-generate common callbacks:
use GenMCP.Suite.Tool,
name: "search_files",
description: "Searches for files matching a pattern",
input_schema: %{
type: :object,
properties: %{query: %{type: :string}},
required: [:query]
}Auto-generates info/2, input_schema/1, and validate_request/2 with
JSON schema validation.
JSV Build Options
The auto-generated validate_request/2 validates incoming arguments against
the :input_schema using a JSV root built at compile time. By default, the
root is built with formats: true and atoms: true.
The :jsv_build_opts option is merged on top of those defaults and the
result is passed to JSV.build/2. Any key given in :jsv_build_opts wins
over the default, so to change :formats or :atoms you must set them
explicitly:
use GenMCP.Suite.Tool,
name: "MyTool",
input_schema: Input,
jsv_build_opts: [
formats: [MyApp.MyFormats | JSV.default_format_validator_modules()],
atoms: false
]See JSV.build/2 for the full list of available options.
Synchronous Tool Example
defmodule MySearchTool do
use GenMCP.Suite.Tool,
name: "search_files",
input_schema: %{
type: :object,
properties: %{
query: %{type: :string}
}
}
alias GenMCP.MCP
@impl true
def call(req, channel, _arg) do
# Arguments are string keys unless using a JSV module based schema or a
# custom validate_request function.
%{"query" => query} = req.params.arguments
text = generate_text_response(query)
{:result, MCP.call_tool_result(text: text), channel}
end
endAsynchronous Tool Example
defmodule MyAsyncTool do
use GenMCP.Suite.Tool,
name: "expensive_search",
input_schema: %{}
alias GenMCP.MCP
@impl true
def call(req, channel, _arg) do
task = Task.async(fn -> perform_expensive_search(req) end)
{:async, {:search_task, task}, channel}
end
@impl true
def continue({:search_task, {:ok, document}}, channel, _arg) do
{:result, MCP.call_tool_result(text: document), channel}
end
end
Summary
Types
This is not supported yet
Callbacks
Executes the tool call and returns a result, error, or async continuation.
Continues processing after async work completes.
Returns tool metadata for the specified key.
Returns the JSON schema defining accepted tool arguments.
Returns the JSON schema defining tool result structure, or nil if
unspecified.
Validates and optionally transforms the incoming call request.
Functions
Invokes the tool's call/3 callback with request validation.
Dispatches continuation logic to the tool's continue/3 callback.
Builds an MCP tool description suitable for tools/list responses.
Returns a descriptor for the given module or {module, arg} tuple.
Types
@type arg() :: term()
@type call_result() :: {:result, GenMCP.MCP.CallToolResult.t(), GenMCP.Mux.Channel.t()} | {:async, {tag(), reference() | Task.t()}, GenMCP.Mux.Channel.t()} | {:error, String.t(), GenMCP.Mux.Channel.t()}
@type client_response() :: term()
This is not supported yet
@type info_key() :: :name | :title | :description | :annotations | :_meta
@type request() :: term()
@type schema() :: term()
@type tag() :: term()
@type tool() :: module() | {module(), arg()} | tool_descriptor()
Callbacks
@callback call(GenMCP.MCP.CallToolRequest.t(), GenMCP.Mux.Channel.t(), arg()) :: call_result()
Executes the tool call and returns a result, error, or async continuation.
Receives validated request parameters if validate_request/2 is defined.
When using use GenMCP.Suite.Tool, JSON schema validation is automatically
implemented, ensuring req.params.arguments conforms to the schema.
Result payload
Synchronous results are returned as {:result, %MCP.CallToolResult{}, channel}. The second element must be a %MCP.CallToolResult{}, typically
built with GenMCP.MCP.call_tool_result/1.
Async calls
When returning {:async, {tag, ref}, channel}, the continue/3 callback
will be invoked with {tag, {:ok, value}} if the server process receives a
{ref, value} message with the same ref.
This works automatically with tasks. It is possible to directly return the
task struct as in {:async, {tag, task}, channel}.
Note that Task.async/1 may crash the server process, you may want to use
Task.Supervisor.async_nolink/2 in which case a :DOWN message from the task
will be delivered as with the same tag as {tag, {:error, reason}}.
This should also work with manually monitored processes, given the monitored
process obtains the ref to send the {ref, result} value back to the calling
process.
Channel and Assigns
The channel provides access to assigns copied from the Plug.Conn struct
(from the HTTP request that delivered the tool call request) and can be
modified via GenMCP.Mux.Channel.assign/3 to keep state before entering the
continue/3 callback.
Assigning modifies the channel, so the last updated channel must always be returned from your callback.
Examples
Synchronous execution:
def call(req, channel, _arg) do
%{"query" => query} = req.params.arguments
entity = perform_search(query)
# With structured output (entity is a map), mind the list wrapper
{:result, MCP.call_tool_result([entity]), channel}
# Without structured output
{:result, MCP.call_tool_result(text: Jason.encode!(entity)), channel}
endAsynchronous with Task:
def call(req, channel, _arg) do
task = Task.async(fn -> expensive_operation(req) end)
{:async, {:search_task, task}, channel}
endError handling:
def call(_req, channel, _arg) do
{:error, "Resource not available", channel}
end
@callback continue( {tag(), {:ok, term()} | {:error, term()}}, GenMCP.Mux.Channel.t(), arg() ) :: call_result()
Continues processing after async work completes.
Invoked when call/3 returns {:async, {tag, ref_or_task}, channel} and
the task finishes.
The first tuple element contains the tag and the wrapped task result (either
{:ok, result} for success or {:error, reason} for failures with non-linked
tasks). Returns same result types as call/3. Can chain another async
operation by returning {:async, {new_tag, new_ref}, channel}.
The tag is generally an atom to be matched on if you implement multiple conitinuation callbacks but it can be any term.
Examples
Basic continuation:
def continue({:search_task, {:ok, results}}, channel, _arg) do
{:result, MCP.call_tool_result(text: Jason.encode!(results)), channel}
endError handling:
def continue({:search_task, {:error, reason}}, channel, _arg) do
{:error, "Search failed: #{reason}", channel}
endChaining async operations:
def continue({:step1, {:ok, intermediate}}, channel, _arg) do
task = Task.async(fn -> step2(intermediate) end)
{:async, {:step2, task}, channel}
end
@callback info(:name, arg()) :: String.t()
@callback info(:description, arg()) :: nil | String.t()
@callback info(:title, arg()) :: nil | String.t()
@callback info(:annotations, arg()) :: nil | tool_annotations()
@callback info(:_meta, arg()) :: nil | map()
Returns tool metadata for the specified key.
Invoked by describe/1 to gather tool metadata.
With use GenMCP.Suite.Tool with metadata options, this callback is
auto-generated.
Examples
def info(:name, _arg), do: "search_files"
def info(:description, _arg), do: "Searches for files matching a pattern"
def info(:_meta, _arg), do: %{"ui/resourceUri" => "ui://pages/some-page"}
def info(:annotations, _arg), do: %{readOnlyHint: true}
def info(_, _), do: nil
Returns the JSON schema defining accepted tool arguments.
Sent to clients in tools/list responses to describe expected parameters.
Auto-generated with use GenMCP.Suite.Tool with an :input_schema option.
Examples
def input_schema(_arg) do
%{
type: :object,
properties: %{
query: %{type: :string},
limit: %{type: :integer, default: 10}
},
required: [:query]
}
end
Returns the JSON schema defining tool result structure, or nil if
unspecified.
Defines the structured outputs returned by the tool if any. Entirely optional and not enforced at runtime.
Auto-generated with use GenMCP.Suite.Tool with an :output_schema option.
Examples
def output_schema(_arg) do
%{
type: :object,
properties: %{
files: %{type: :array, items: %{type: :string}}
}
}
end
@callback validate_request(GenMCP.MCP.CallToolRequest.t(), arg()) :: {:ok, GenMCP.MCP.CallToolRequest.t()} | {:error, String.t()}
Validates and optionally transforms the incoming call request.
Invoked before call/3.
Auto-generated with JSON schema validation when using use GenMCP.Suite.Tool
with an :input_schema option.
Examples
def validate_request(req, _arg) do
case req.params.arguments do
%{"limit" => n} when n > 0 and n <= 100 -> {:ok, req}
_ -> {:error, "limit must be between 1 and 100"}
end
end
Functions
@spec call(tool_descriptor(), GenMCP.MCP.CallToolRequest.t(), GenMCP.Mux.Channel.t()) :: call_result()
Invokes the tool's call/3 callback with request validation.
Performs optional validation via validate_request/2 before dispatching to
the tool implementation. Returns {:error, {:invalid_params, reason}, channel} if validation fails. Called by GenMCP.Suite when handling
CallToolRequest.
Examples
req = %GenMCP.MCP.CallToolRequest{
params: %{name: "search_files", arguments: %{"query" => "*.ex"}}
}
Tool.call(tool_descriptor, req, channel)
#=> {:result, %GenMCP.MCP.CallToolResult{...}, channel}
# With validation error
Tool.call(tool_descriptor, invalid_req, channel)
#=> {:error, {:invalid_params, "limit must be positive"}, channel}
@spec continue( tool_descriptor(), {tag(), client_response() | term()}, GenMCP.Mux.Channel.t() ) :: call_result()
Dispatches continuation logic to the tool's continue/3 callback.
Invoked by GenMCP.Suite when an async task completes. The continuation tuple
contains the tag from the original {:async, {tag, ref}, channel} return and
the wrapped task result. Task results are wrapped as {:ok, result} for
normal completion or {:error, reason} for failures.
Examples
Tool.continue(tool_descriptor, {:search_task, {:ok, results}}, channel)
#=> {:result, %GenMCP.MCP.CallToolResult{...}, channel}
Tool.continue(tool_descriptor, {:search_task, {:error, :timeout}}, channel)
#=> {:error, "Search timed out", channel}
@spec describe(tool()) :: GenMCP.MCP.Tool.t()
Builds an MCP tool description suitable for tools/list responses.
Gathers metadata via info/2 callbacks and normalizes input/output schemas
to JSON schema format. Invoked by GenMCP.Suite when handling
ListToolsRequest.
Examples
iex> Tool.describe(MySearchTool)
%GenMCP.MCP.Tool{
name: "search_files",
description: "Searches for files matching a pattern",
inputSchema: %{"type" => "object", "properties" => ...},
annotations: %{readOnlyHint: true}
}
iex> Tool.describe({MySearchTool, [repo_path: "/data"]})
%GenMCP.MCP.Tool{name: "search_files", ...}
@spec expand(tool()) :: tool_descriptor()
Returns a descriptor for the given module or {module, arg} tuple.