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]
endA 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
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
@callback assigns(ctx :: Sake.Context.t()) :: map() | keyword()
The assigns of a template:, as a map or a keyword list.
@callback render(ctx :: Sake.Context.t()) :: iodata() | {:safe, iodata()} | struct()
The HTML document of the app.
Functions
@spec client_capability() :: map()
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.
@spec extension() :: String.t()
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.
<script><%= raw inline("priv/static/assets/sales_map.js") %></script>
@spec mime_type() :: String.t()
The MIME type of an MCP App document.
Marks iodata as HTML that a template must not escape.
@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.