Behaviour for defining individual MCP tools, resources, and prompts as separate modules.
Each component is a standalone module with its own schema and execution logic.
Components are registered with a ConduitMcp.Endpoint which aggregates them
into a full MCP server.
Component Types
:tool— An MCP tool with input parameters and execution logic:resource— An MCP resource identified by a URI template:prompt— An MCP prompt with arguments that returns messages
Tool Example
defmodule MyApp.Echo do
use ConduitMcp.Component, type: :tool, description: "Echoes text back"
schema do
field :text, :string, "The text to echo", required: true, max_length: 150
end
@impl true
def execute(%{text: text}, _conn) do
text(text)
end
endResource Example
defmodule MyApp.ReadUser do
use ConduitMcp.Component,
type: :resource,
uri: "user://{id}",
description: "Reads a user by ID",
mime_type: "application/json"
@impl true
def execute(%{id: id}, _conn) do
user = MyApp.Users.get!(id)
{:ok, %{"contents" => [%{
"uri" => "user://#{id}",
"mimeType" => "application/json",
"text" => JSON.encode!(user)
}]}}
end
endPrompt Example
defmodule MyApp.CodeReview do
use ConduitMcp.Component, type: :prompt, description: "Code review assistant"
schema do
field :code, :string, "Code to review", required: true
field :language, :string, "Programming language", default: "elixir"
end
@impl true
def execute(%{code: code, language: language}, _conn) do
{:ok, %{"messages" => [
system("You are a code reviewer for #{language}"),
user("Review this code:\n#{code}")
]}}
end
endOptions
:type— Required. One of:tool,:resource,:prompt:name— Optional. Defaults to last module segment, snake_cased:description— Required for tools and prompts:uri— Required for resources. URI template with{param}placeholders:mime_type— Optional for resources:scope— Optional space-separated OAuth scope string. Enforced byConduitMcp.Handlerfor tools, resources and prompts; a request lacking the scope is rejected beforeexecute/2runs, and a request with no principal fails closed.:annotations— Optional keyword list of tool annotations (e.g.,destructive: true):cache— Optional modern-era cache hints,[ttl_ms: non_neg_integer, scope: :private | :public]; seeConduitMcp.CacheHints.scope: :publicwith a:scopeis a compile error.
Generated Functions
After compilation, each component module exports:
__component_type__/0— Returns:tool,:resource, or:prompt__component_name__/0— Returns the string name__component_description__/0— Returns the description__component_schema__/0— Returns the JSON Schema map for MCP clients__validation_schema__/0— Returns the NimbleOptions schema for runtime validation__component_opts__/0— Returns raw component options
Summary
Callbacks
Called to execute the component's logic.
Callbacks
@callback execute(params :: map(), conn :: Plug.Conn.t()) :: {:ok, map()} | {:error, map()} | ConduitMcp.Server.input_required() | ConduitMcp.Server.task()
Called to execute the component's logic.
For tools: receives validated params (atom-keyed) and the Plug.Conn. For resources: receives URI params (atom-keyed) extracted from the template and the Plug.Conn. For prompts: receives validated args (atom-keyed) and the Plug.Conn.
Must return {:ok, map()} or {:error, map()}. On a modern-era
(2026-07-28) request it may instead return
{:input_required, input_requests, state} to ask the client for input;
ConduitMcp.Endpoint passes it through, and the retry finds the restored
state and the answers in conn.assigns[:mcp_input]. See "Multi round-trip
requests (MRTR)" in ConduitMcp.Server:
@impl true
def execute(%{target: target}, conn) do
case conn.assigns[:mcp_input] do
nil ->
{:input_required,
%{"approval" => ConduitMcp.MRTR.elicit("Approve deploy?", @approval_schema)},
%{target: target}}
%{responses: %{"approval" => %{"action" => "accept"}}} ->
text("Deployed #{target}")
_declined ->
text("Cancelled")
end
end