Helper macros for building MCP responses in the DSL.
These helpers provide a convenient way to construct properly formatted MCP responses without manually building the response maps.
Response Helpers
text/1- Returns a text content responsejson/1- Returns JSON-encoded text contentraw/1- Returns raw data directly (bypasses MCP content wrapping)error/1orerror/2- Returns an error responseimage/1- Returns an image content response
Prompt Message Helpers
system/1- Creates a system role messageuser/1- Creates a user role messageassistant/1- Creates an assistant role message
Examples
# Text response
text("Hello, world!")
# => {:ok, %{"content" => [%{"type" => "text", "text" => "Hello, world!"}]}}
# JSON response
json(%{status: "ok", count: 42})
# => {:ok, %{"content" => [%{"type" => "text", "text" => "{\"status\":\"ok\",\"count\":42}"}]}}
# Raw response (bypasses MCP wrapping)
raw(%{status: "ok", count: 42})
# => {:ok, %{"status" => "ok", "count" => 42}}
# Error response
error("Not found")
# => {:error, %{"code" => -32000, "message" => "Not found"}}
# Custom error code
error("Invalid params", -32602)
# => {:error, %{"code" => -32602, "message" => "Invalid params"}}
# Prompt messages
[
system("You are a helpful assistant"),
user("What is 2+2?")
]
Summary
Functions
Creates a resource content response for MCP Apps UI.
Creates an assistant role message for prompts.
Creates an audio content response.
Creates an error response.
Reports a tool execution error to the client.
Creates an image content response.
Creates a JSON-encoded text content response.
Returns raw data directly without MCP content wrapping.
Creates a resource content response with a specified MIME type.
Returns a tool response with structured output.
Creates a system role message for prompts.
Creates an MCP tools/call response that hands a long-running operation
off to a task.
Creates a text content response.
Creates multiple text content items.
Creates a user role message for prompts.
Functions
Creates a resource content response for MCP Apps UI.
Shortcut for raw_resource(content, "text/html;profile=mcp-app").
The text/html;profile=mcp-app MIME type is required by MCP Apps hosts
to render the HTML as a sandboxed iframe.
Example
resource "ui://dashboard/app.html" do
mime_type "text/html;profile=mcp-app"
read fn _conn, _params, _opts ->
html = File.read!("priv/mcp_apps/dashboard.html")
app_html(html)
end
end
Creates an assistant role message for prompts.
Example
def handle_get_prompt(_conn, "example", _args) do
{:ok, %{
"messages" => [
user("Show me an example"),
assistant("Here's an example: ...")
]
}}
end
Creates an audio content response.
Example
handle fn _conn, %{"file" => file} ->
data = File.read!(file) |> Base.encode64()
audio(data, "audio/wav")
end
Creates an error response.
Examples
error("User not found")
# => {:error, %{"code" => -32000, "message" => "User not found"}}
error("Invalid parameters", -32602)
# => {:error, %{"code" => -32602, "message" => "Invalid parameters"}}
Reports a tool execution error to the client.
Per MCP spec, tool execution errors (the operation ran but failed in a way
the LLM can interpret and possibly recover from) are distinct from protocol
errors. They are returned as a successful result with "isError" => true,
not as a JSON-RPC error. Use error/2 for protocol errors (bad params,
internal failures, etc.) and execution_error/1 for "the call ran but
this is what went wrong" so the LLM can self-correct.
Example
tool "fetch_user", "Fetch a user by id" do
param :id, :string, "User id", required: true
handle fn _conn, %{"id" => id} ->
case MyUsers.fetch(id) do
{:ok, user} -> json(user)
{:error, :not_found} -> execution_error("User #{id} not found")
end
end
end
Creates an image content response.
Example
def handle_call_tool(_conn, "generate_chart", params) do
image_url = MyCharts.generate(params)
image(image_url)
end
Creates a JSON-encoded text content response.
The data will be encoded to JSON using the built-in JSON module.
Example
def handle_call_tool(_conn, "get_user", %{"id" => id}) do
user = MyApp.Users.get!(id)
json(%{id: user.id, name: user.name, email: user.email})
end
Returns raw data directly without MCP content wrapping.
This bypasses the standard MCP content structure and returns the data as-is. Useful for debugging or special cases where you need direct JSON output without the content array wrapper.
Warning: This breaks MCP compatibility and should only be used for debugging or non-MCP endpoints.
Example
def handle_call_tool(_conn, "debug_user", %{"id" => id}) do
user = MyApp.Users.get!(id)
raw(%{id: user.id, name: user.name, email: user.email})
end
# Returns: {:ok, %{"id" => 123, "name" => "John", "email" => "john@example.com"}}
# Instead of: {:ok, %{"content" => [%{"type" => "text", "text" => "{\"id\":123,...}"}]}}
Creates a resource content response with a specified MIME type.
Useful for returning raw HTML, XML, or other content types from
resource read handlers. For MCP Apps ui:// resources, prefer
app_html/1 which uses the correct MIME type automatically.
Example
resource "config://settings.xml" do
mime_type "application/xml"
read fn _conn, _params, _opts ->
xml = File.read!("priv/settings.xml")
raw_resource(xml, "application/xml")
end
end
Returns a tool response with structured output.
Per MCP spec 2025-11-25, tools can declare an outputSchema and return
structured data alongside the human-readable content array. Clients
that understand the schema can render/validate the payload; clients
that don't fall back to the text content.
Accepts the structured payload (any JSON-encodable map) and an optional human-readable message that becomes the text content. If you omit the message, the JSON encoding of the payload is used.
Example
tool "get_user", "Fetch a user" do
param :id, :string, "User id", required: true
output_schema %{
"type" => "object",
"properties" => %{
"id" => %{"type" => "string"},
"email" => %{"type" => "string"}
}
}
handle fn _conn, %{"id" => id} ->
user = MyUsers.get!(id)
structured(%{"id" => user.id, "email" => user.email}, "Fetched user #{id}")
end
end
Creates a system role message for prompts.
Example
def handle_get_prompt(_conn, "assistant", _args) do
{:ok, %{
"messages" => [
system("You are a helpful coding assistant")
]
}}
end
Creates an MCP tools/call response that hands a long-running operation
off to a task.
The tool returns immediately with a task_id; the client polls
tasks/get / tasks/result to retrieve progress and the final result.
The task itself is tracked by ConduitMcp.Tasks — the tool author is
responsible for spawning the actual work (e.g., via Task.Supervisor)
and updating the task's status with ConduitMcp.Tasks.update/2.
Requires the tool's task_support to be :supported or :required.
Example
tool "render_video", "Render a video" do
task_support :supported
param :script, :string, "Script", required: true
handle fn _conn, params ->
task_id = ConduitMcp.Tasks.generate_id()
{:ok, _} = ConduitMcp.Tasks.create(task_id, %{"tool" => "render_video"})
Task.Supervisor.start_child(MyApp.Workers, fn ->
result = MyRenderer.render(params)
ConduitMcp.Tasks.update(task_id,
%{"status" => "completed", "result" => result})
end)
task(task_id, "Rendering started")
end
end
Creates a text content response.
Example
def handle_call_tool(_conn, "greet", %{"name" => name}) do
text("Hello, #{name}!")
end
Creates multiple text content items.
Useful for returning multiple pieces of content in a single response.
Example
def handle_call_tool(_conn, "analyze", params) do
results = MyAnalyzer.run(params)
{:ok, %{
"content" => texts([
"Analysis Results:",
"Score: #{results.score}",
"Details: #{results.details}"
])
}}
end
Creates a user role message for prompts.
Example
def handle_get_prompt(_conn, "question", args) do
{:ok, %{
"messages" => [
user("What is #{args["topic"]}?")
]
}}
end