Current section

Files

Jump to
hermes_mcp lib hermes server response.ex
Raw

lib/hermes/server/response.ex

defmodule Hermes.Server.Response do
@moduledoc """
Fluent interface for building MCP component responses.
This module provides builders for tool, prompt, and resource responses
that integrate seamlessly with the component system.
## Examples
# Tool response
Response.tool()
|> Response.text("Result: " <> result)
|> Response.build()
# Resource response (uri and mime_type come from component)
Response.resource()
|> Response.text(file_contents)
|> Response.build()
# Prompt response
Response.prompt()
|> Response.user_message("What is the weather?")
|> Response.assistant_message("Let me check...")
|> Response.build()
"""
@type t :: %__MODULE__{
type: :tool | :prompt | :resource,
content: list(map),
messages: list(map),
contents: list(map) | nil,
isError: boolean,
metadata: map
}
defstruct [
:type,
content: [],
messages: [],
contents: nil,
isError: false,
metadata: %{}
]
@doc """
Start building a tool response.
## Examples
iex> Response.tool()
%Response{type: :tool, content: [], isError: false}
"""
def tool, do: %__MODULE__{type: :tool}
@doc """
Start building a prompt response with optional description.
## Parameters
* `description` - Optional description of the prompt
## Examples
iex> Response.prompt()
%Response{type: :prompt, messages: []}
iex> Response.prompt("Weather assistant prompt")
%Response{type: :prompt, messages: [], description: "Weather assistant prompt"}
"""
def prompt(description \\ nil) do
response = %__MODULE__{type: :prompt}
if description, do: Map.put(response, :description, description), else: response
end
@doc """
Start building a resource response.
The uri and mimeType are automatically injected from the component's
uri/0 and mime_type/0 callbacks when the response is built by the server.
## Examples
iex> Response.resource()
%Response{type: :resource, contents: nil}
"""
def resource, do: %__MODULE__{type: :resource}
@doc """
Add text content to a tool or resource response.
For tool responses, adds text to the content array.
For resource responses, sets the text content.
## Parameters
* `response` - A tool or resource response struct
* `text` - The text content
## Examples
iex> Response.tool() |> Response.text("Hello world")
%Response{
type: :tool,
content: [%{"type" => "text", "text" => "Hello world"}],
isError: false
}
iex> Response.resource() |> Response.text("File contents")
%Response{type: :resource, contents: %{"text" => "File contents"}}
"""
def text(%{type: :tool} = r, text) when is_binary(text) do
add_content(r, %{"type" => "text", "text" => text})
end
def text(%{type: :resource} = r, text) when is_binary(text) do
%{r | contents: %{"text" => text}}
end
@doc """
Add JSON-encoded content to a tool response.
This is a convenience function that automatically encodes data as JSON
and adds it as text content. Useful for returning structured data from tools.
## Parameters
* `response` - A tool response struct
* `data` - Any JSON-encodable data structure
## Examples
iex> Response.tool() |> Response.json(%{status: "ok", count: 42})
%Response{
type: :tool,
content: [%{"type" => "text", "text" => "{\\"status\\":\\"ok\\",\\"count\\":42}"}],
isError: false
}
iex> Response.tool() |> Response.json([1, 2, 3])
%Response{
type: :tool,
content: [%{"type" => "text", "text" => "[1,2,3]"}],
isError: false
}
"""
def json(%{type: :tool} = r, data) do
add_content(r, %{"type" => "text", "text" => JSON.encode!(data)})
end
def json(%{type: :resource} = r, data) do
%{r | contents: %{"text" => JSON.encode!(data)}}
end
@doc """
Add image content to a tool response.
## Parameters
* `response` - A tool response struct
* `data` - Base64 encoded image data
* `mime_type` - MIME type of the image (e.g., "image/png")
## Examples
iex> Response.tool() |> Response.image(base64_data, "image/png")
%Response{
type: :tool,
content: [%{"type" => "image", "data" => base64_data, "mimeType" => "image/png"}],
isError: false
}
"""
def image(%{type: :tool} = r, data, mime_type)
when is_binary(data) and is_binary(mime_type) do
add_content(r, %{"type" => "image", "data" => data, "mimeType" => mime_type})
end
@doc """
Add audio content to a tool response.
## Parameters
* `response` - A tool response struct
* `data` - Base64 encoded audio data
* `mime_type` - MIME type of the audio (e.g., "audio/wav")
* `opts` - Optional keyword list with:
* `:transcription` - Optional text transcription of the audio
## Examples
iex> Response.tool() |> Response.audio(audio_data, "audio/wav")
%Response{
type: :tool,
content: [%{"type" => "audio", "data" => audio_data, "mimeType" => "audio/wav"}],
isError: false
}
iex> Response.tool() |> Response.audio(audio_data, "audio/wav", transcription: "Hello")
%Response{
type: :tool,
content: [%{
"type" => "audio",
"data" => audio_data,
"mimeType" => "audio/wav",
"transcription" => "Hello"
}],
isError: false
}
"""
def audio(%{type: :tool} = r, data, mime_type, opts \\ []) do
content = %{"type" => "audio", "data" => data, "mimeType" => mime_type}
content =
if opts[:transcription],
do: Map.put(content, "transcription", opts[:transcription]),
else: content
add_content(r, content)
end
@doc """
Add an embedded resource reference to a tool response.
## Parameters
* `response` - A tool response struct
* `uri` - The resource URI
* `opts` - Optional keyword list with:
* `:name` - Human-readable name
* `:description` - Resource description
* `:mime_type` - MIME type
* `:text` - Text content (for text resources)
* `:blob` - Base64 data (for binary resources)
## Examples
iex> Response.tool() |> Response.embedded_resource("file://example.txt",
...> name: "Example File",
...> mime_type: "text/plain",
...> text: "File contents"
...> )
"""
def embedded_resource(%{type: :tool} = r, uri, opts \\ []) do
resource =
%{"uri" => uri}
|> maybe_put("name", opts[:name])
|> maybe_put("description", opts[:description])
|> maybe_put("mimeType", opts[:mime_type])
|> maybe_put("text", opts[:text])
|> maybe_put("blob", opts[:blob])
add_content(r, %{"type" => "resource", "resource" => resource})
end
@doc """
Mark a tool response as an error and add error message.
## Parameters
* `response` - A tool response struct
* `message` - The error message
## Examples
iex> Response.tool() |> Response.error("Division by zero")
%Response{
type: :tool,
content: [%{"type" => "text", "text" => "Error: Division by zero"}],
isError: true
}
"""
def error(%{type: :tool} = r, message) when is_binary(message) do
r
|> text(message)
|> Map.put(:isError, true)
end
@doc """
Add a user message to a prompt response.
## Parameters
* `response` - A prompt response struct
* `content` - The message content (string or structured content)
## Examples
iex> Response.prompt() |> Response.user_message("What's the weather?")
%Response{
type: :prompt,
messages: [%{"role" => "user", "content" => "What's the weather?"}]
}
"""
def user_message(%{type: :prompt} = r, content) do
add_message(r, %{"role" => "user", "content" => build_message_content(content)})
end
@doc """
Add an assistant message to a prompt response.
## Parameters
* `response` - A prompt response struct
* `content` - The message content (string or structured content)
## Examples
iex> Response.prompt() |> Response.assistant_message("Let me check the weather for you.")
%Response{
type: :prompt,
messages: [%{"role" => "assistant", "content" => "Let me check the weather for you."}]
}
"""
def assistant_message(%{type: :prompt} = r, content) do
add_message(r, %{
"role" => "assistant",
"content" => build_message_content(content)
})
end
@doc """
Add a system message to a prompt response.
## Parameters
* `response` - A prompt response struct
* `content` - The message content (string or structured content)
## Examples
iex> Response.prompt() |> Response.system_message("You are a helpful weather assistant.")
%Response{
type: :prompt,
messages: [%{"role" => "system", "content" => "You are a helpful weather assistant."}]
}
"""
def system_message(%{type: :prompt} = r, content) do
add_message(r, %{"role" => "system", "content" => build_message_content(content)})
end
@doc """
Set blob (base64) content for a resource response.
## Parameters
* `response` - A resource response struct
* `data` - Base64 encoded binary data
## Examples
iex> Response.resource() |> Response.blob(base64_data)
%Response{type: :resource, contents: %{"blob" => base64_data}}
"""
def blob(%{type: :resource} = r, data) when is_binary(data) do
%{r | contents: %{"blob" => data}}
end
@doc """
Set optional name for a resource response.
## Parameters
* `response` - A resource response struct
* `name` - Human-readable name for the resource
## Examples
iex> Response.resource() |> Response.name("Configuration File")
%Response{type: :resource, metadata: %{name: "Configuration File"}}
"""
def name(%{type: :resource} = r, name) when is_binary(name) do
put_metadata(r, :name, name)
end
@doc """
Set optional description for a resource response.
## Parameters
* `response` - A resource response struct
* `desc` - Description of the resource
## Examples
iex> Response.resource() |> Response.description("Application configuration settings")
%Response{type: :resource, metadata: %{description: "Application configuration settings"}}
"""
def description(%{type: :resource} = r, desc) when is_binary(desc) do
put_metadata(r, :description, desc)
end
@doc """
Build the final response structure.
Transforms the response struct into the appropriate format for the MCP protocol.
## Parameters
* `response` - A response struct of any type
## Examples
iex> Response.tool() |> Response.text("Hello") |> Response.to_protocol()
%{"content" => [%{"type" => "text", "text" => "Hello"}], "isError" => false}
iex> Response.prompt() |> Response.user_message("Hi") |> Response.to_protocol()
%{"messages" => [%{"role" => "user", "content" => "Hi"}]}
iex> Response.resource() |> Response.text("data") |> Response.to_protocol()
%{"text" => "data"}
"""
def to_protocol(%{type: :tool} = r) do
%{"content" => r.content, "isError" => r.isError}
end
def to_protocol(%{type: :prompt} = r) do
base = %{"messages" => r.messages}
if Map.get(r, :description),
do: Map.put(base, "description", r.description),
else: base
end
def to_protocol(%{type: :resource} = r, uri, mime_type) do
string_metadata =
Map.new(r.metadata, fn {k, v} -> {to_string(k), v} end)
r.contents
|> Map.merge(string_metadata)
|> Map.put("uri", uri)
|> Map.put("mimeType", mime_type)
end
defp add_content(r, content), do: %{r | content: r.content ++ [content]}
defp add_message(r, message), do: %{r | messages: r.messages ++ [message]}
defp put_metadata(r, key, value),
do: %{r | metadata: Map.put(r.metadata, key, value)}
defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, key, value), do: Map.put(map, key, value)
defp build_message_content(text) when is_binary(text), do: text
defp build_message_content(content), do: content
end