ConduitMcp.DSL.Helpers (ConduitMCP v0.9.4)

Copy Markdown View Source

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 response
  • json/1 - Returns JSON-encoded text content
  • raw/1 - Returns raw data directly (bypasses MCP content wrapping)
  • error/1 or error/2 - Returns an error response
  • image/1 - Returns an image content response

Prompt Message Helpers

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.

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

app_html(content)

(macro)

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

assistant(content)

(macro)

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

audio(data, mime_type)

(macro)

Creates an audio content response.

Example

handle fn _conn, %{"file" => file} ->
  data = File.read!(file) |> Base.encode64()
  audio(data, "audio/wav")
end

error(message, code \\ ConduitMcp.Errors.server_error())

(macro)

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"}}

execution_error(message)

(macro)

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

image(url)

(macro)

Creates an image content response.

Example

def handle_call_tool(_conn, "generate_chart", params) do
  image_url = MyCharts.generate(params)
  image(image_url)
end

json(data)

(macro)

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

raw(data)

(macro)

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,...}"}]}}

raw_resource(content, mime_type)

(macro)

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

structured(payload, message \\ nil)

(macro)

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

system(content)

(macro)

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

task(task_id, message \\ "Task started")

(macro)

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

text(content)

(macro)

Creates a text content response.

Example

def handle_call_tool(_conn, "greet", %{"name" => name}) do
  text("Hello, #{name}!")
end

texts(string_list)

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

user(content)

(macro)

Creates a user role message for prompts.

Example

def handle_get_prompt(_conn, "question", args) do
  {:ok, %{
    "messages" => [
      user("What is #{args["topic"]}?")
    ]
  }}
end