Sake.UI behaviour (Sake v0.4.3)

Copy Markdown View Source

An MCP App: an HTML document the host renders in a sandboxed iframe when a tool that links it is called (the io.modelcontextprotocol/ui extension). On the wire it is a resource with a ui:// URI and the text/html;profile=mcp-app MIME type, so it is listed on a server with the other resources:

defmodule MyApp.MCP.SalesMap do
  use Sake.UI,
    uri: "ui://my-app/sales-map",
    name: "sales_map",
    title: "Sales map",
    csp: [resource_domains: ["https://tile.openstreetmap.org"]],
    prefers_border: true

  @impl true
  def render(_ctx), do: ~s(<!doctype html><html>...</html>)
end

defmodule MyApp.MCP do
  use Sake.Server, name: "my-app", version: "1.0.0"
  tools [MyApp.MCP.ShowSales]
  resources [MyApp.MCP.SalesMap]
end

A tool shows it with use Sake.Tool, ui: MyApp.MCP.SalesMap.

Options: uri (required, ui://), name (default: the module's last segment in snake case), title, description, icons, scope, ttl, meta, csp (connect_domains, resource_domains, frame_domains, base_uri_domains: the origins the document may reach), permissions (:camera, :microphone, :geolocation, :clipboard_write), domain (a dedicated sandbox origin, in the host's own format), prefers_border, and template.

The document comes from render/1, which returns a string or iodata, {:safe, iodata}, or, when the host has phoenix_html, anything that implements Phoenix.HTML.Safe (the result of a HEEx function component), or from an EEx file named by template:, relative to the module's own file and compiled into the module. A template's assigns come from assigns/1, and <%= %> escapes its output as HTML unless the value is raw/1. inline/1 reads a file into the document at compile time (a bundled script or stylesheet), and the module recompiles when the file or the template changes.

The host learns the document's CSP and permissions from the resource's _meta.ui, on its resources/list entry and on its resources/read contents alike. A client that did not declare the extension does not see UI resources listed (it can still read them).

Summary

Callbacks

The assigns of a template:, as a map or a keyword list.

The HTML document of the app.

Functions

The client capability a host declares to render MCP Apps (the one Sake.Client.connect/2 declares with ui: true).

Escapes a value for an HTML document; {:safe, iodata} passes through.

The identifier of the MCP Apps extension.

The contents of path, relative to the project's root, read at compile time: the module recompiles when the file changes. For the bundled script or stylesheet of the app, so that the document is self-contained.

The MIME type of an MCP App document.

Marks iodata as HTML that a template must not escape.

Whether the client renders MCP Apps: it declared the extension, with the text/html;profile=mcp-app MIME type among its mimeTypes. A tool can use it to word its text for a client that shows no UI.

Callbacks

assigns(ctx)

(optional)
@callback assigns(ctx :: Sake.Context.t()) :: map() | keyword()

The assigns of a template:, as a map or a keyword list.

render(ctx)

(optional)
@callback render(ctx :: Sake.Context.t()) :: iodata() | {:safe, iodata()} | struct()

The HTML document of the app.

Functions

client_capability()

@spec client_capability() :: map()

The client capability a host declares to render MCP Apps (the one Sake.Client.connect/2 declares with ui: true).

escape(value)

@spec escape(term()) :: String.t()

Escapes a value for an HTML document; {:safe, iodata} passes through.

extension()

@spec extension() :: String.t()

The identifier of the MCP Apps extension.

inline(path)

(macro)

The contents of path, relative to the project's root, read at compile time: the module recompiles when the file changes. For the bundled script or stylesheet of the app, so that the document is self-contained.

<script><%= raw inline("priv/static/assets/sales_map.js") %></script>

mime_type()

@spec mime_type() :: String.t()

The MIME type of an MCP App document.

raw(safe)

@spec raw(iodata() | {:safe, iodata()}) :: {:safe, iodata()}

Marks iodata as HTML that a template must not escape.

supported?(context)

@spec supported?(Sake.Context.t()) :: boolean()

Whether the client renders MCP Apps: it declared the extension, with the text/html;profile=mcp-app MIME type among its mimeTypes. A tool can use it to word its text for a client that shows no UI.