Apps Extension - ChristianPresley/zig-sdk GitHub Wiki

Apps Extension

The MCP Apps extension lets a server give interactive views to a host1. A view is an HTML document in a ui:// resource. A tool names its view in _meta.ui.resourceUri. A host that declares the extension shows the view in a sandboxed frame and sends the tool result to it. The extension is optional. The server advertises it with the apps option.

Applies to: MCP 2026-07-28, extension io.modelcontextprotocol/ui (SEP-18652), zig-sdk 0.2.0.

Scope

The SDK provides the parts of the extension between a client and a server:

  • The registration of views and the UI metadata of resources and tools.
  • The client declaration with mimeTypes.
  • The fallback for clients without the extension.
  • Typed access to the UI metadata on the client.

Other parts of the extension are host behavior in a browser2. These parts are out of the scope of the SDK. A host application provides them:

  • The ui/ messages between a host and a view over postMessage.
  • The sandbox proxy.
  • The content security policy and the permission policy.
  • The display modes and the theme.

Enable the extension

var server = try mcp.Server.init(gpa, io, .{
    .info = .{ .name = "weather", .version = "0.1.0" },
    .apps = .{},
});
try server.addUiResource(.{
    .uri = "ui://weather/dashboard",
    .name = "weather_dashboard",
    .meta = .{
        .csp = .{ .connectDomains = &.{"https://api.example.com"} },
        .prefersBorder = true,
    },
}, @embedFile("dashboard.html"));
try server.addToolJson(.{
    .name = "get_weather",
    .ui = .{ .resourceUri = "ui://weather/dashboard" },
}, getWeather);
try server.addToolJson(.{
    .name = "refresh_dashboard",
    .ui = .{ .resourceUri = "ui://weather/dashboard", .visibility = &.{.app} },
}, refreshDashboard);
try server.checkUiLinks(null);

With the apps option, the server advertises the extension under capabilities.extensions with an empty settings object. The option has one field, fallback_for_other_clients, see Clients without the extension.

The registration functions work also without the apps option. Then the server does not advertise the extension, and every client gets the full UI metadata.

Views

addUiResource registers a view with static HTML. addUiResourceHandler registers a view whose content a handler gives. The URI must start with ui:// and have a path. Else the function gives error.InvalidUiMeta.

The server gives each content item the MIME type text/html;profile=mcp-app. A handler can leave the MIME type empty, and the server then sets it. A handler that gives another MIME type causes error -32603.

The field meta holds the UI metadata of the view:

Field Meaning
csp.connectDomains The origins for network requests.
csp.resourceDomains The origins for scripts, images, styles, fonts and media.
csp.frameDomains The origins for nested frames.
csp.baseUriDomains The permitted base URIs of the document.
permissions The browser permissions that the view asks for: camera, microphone, geolocation and clipboardWrite.
domain A dedicated origin for the view. The format depends on the host.
prefersBorder True asks for a border and a background. Null lets the host decide.

The server puts the metadata on each content item of resources/read. For a handler view, the server adds it only to the items without _meta, and keeps the _meta of the handler. The field meta_in_listing of the view is true by default. Then the server also puts the metadata on the entry in resources/list. A host can then examine the security configuration before it reads the view.

Tools

ToolDef.ui holds the UI metadata of a tool. The server puts it under _meta.ui:

Field Meaning
resourceUri The view of the tool. It must be a ui:// URI.
visibility model, app or both. Null means both. model lets the model see and call the tool. app lets a view of the same server call the tool.

ToolDef.meta holds other _meta keys. A ui key in meta together with ui gives error.InvalidMeta. A meta that is not an object also gives error.InvalidMeta. A resourceUri that is not a ui:// URI, or an empty or repeated visibility list, gives error.InvalidUiMeta.

The extension requires that the view of a tool exists on the server. checkUiLinks(broken_tool) checks each resourceUri against the resources and the resource templates of the server. A missing view gives error.UnknownUiResource. When broken_tool is not null, the function writes the name of the tool through it.

A tool with a view must still return meaningful content. A host without the extension shows only that content.

Clients without the extension

A client declares the extension on each request in _meta["io.modelcontextprotocol/clientCapabilities"], under extensions["io.modelcontextprotocol/ui"].mimeTypes. The option fallback_for_other_clients is on by default. The server then treats a client without text/html;profile=mcp-app in that list as a text-only client:

  • tools/list gives the tools without _meta.ui and without the old key ui/resourceUri. When no other key stays, the tool has no _meta.
  • tools/list does not give the tools that only a view can call.
  • A tools/call of such a tool gives error -32602 with the message Unknown tool: NAME, as for a tool that does not exist.

The fallback applies only to tools with ToolDef.ui. The server does not filter resources/list. With the option off, every client gets the full metadata.

The server does not apply the visibility to a client that declared the extension. That client can call a tool with the visibility app directly. The host must apply the visibility rules.

The client

The client declares the extension with mcp.protocol.apps.declare. The function keeps the other extensions of the capabilities.

var client: mcp.Client = .init(gpa, io, .{
    .info = .{ .name = "host", .version = "0.1.0" },
    .capabilities = try mcp.protocol.apps.declare(arena, .{}),
});

The declaration is {"mimeTypes": ["text/html;profile=mcp-app"]} under extensions. These functions then give the data of the extension:

Function Effect
client.toolUi(arena, tool) The UI metadata of a tool, or null when the tool has none or the client did not declare the extension. Bad metadata gives error.InvalidUiMeta.
client.readUiResource(arena, uri, listing_meta, options) Reads a view with resources/read. It checks the URI and the MIME type and gives the URI, the HTML and the UI metadata.
mcp.protocol.apps.modelTools(arena, tools) The tools that the host can give to the model.
mcp.protocol.apps.resourceMeta(arena, content_meta, listing_meta) The UI metadata of a view. The object of the content item replaces the object of the resources/list entry. The function does not merge the fields.

readUiResource gives error.NotUiUri before it sends the request. After the request, it can give error.MissingContent, error.WrongMimeType or error.InvalidBlob. A content item without a MIME type also gives error.WrongMimeType.

A host must not give a tool to the model when the visibility of the tool does not include model. A host must refuse a call from a view to a tool when the visibility does not include app. modelTools and ToolMeta.callableByApp give the data for these rules.

The client also reads the deprecated flat key _meta["ui/resourceUri"] as a fallback. The server never writes it.

Conformance

The official conformance suite has no scenarios for the extension.

References

  1. MCP-EXT-APPS: MCP Apps extension specification.
  2. MCP-SEP-1865: SEP-1865, MCP Apps.
⚠️ **GitHub.com Fallback** ⚠️