From ca6bd5cea007f5324c737185136261d5facc3dd5 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 00:18:26 +0700 Subject: [PATCH] spec(t27mcp): visual_output.t27 -- when answer_channel picks UI_RESOURCE An MCP Apps host (a client whose initialize capabilities name the io.modelcontextprotocol/ui extension with the text/html;profile=mcp-app mime) gets a t27f drawing as a ui:// resource; with no drawing it gets the markdown table. client_host() reads the two capability facts, carries_ui_resource() says whether a t27f result adds the resource, and UI_PAGE / UI_URI_VIEW / UI_URI_GRAPH / UI_EMPTY are the page and addresses the server serves. Tests: 12/12, 0 vacuous, on the Railway lab. Mutants: an MCP Apps resource with no drawing fails 2, client_host ignoring the mime fails 2, the readability mutant still fails 3. Seal regenerated. Co-Authored-By: Claude Opus 5.5 --- .trinity/seals/t27mcp_T27mcpVisualOutput.json | 16 ++--- specs/t27mcp/visual_output.t27 | 72 ++++++++++++++++++- 2 files changed, 79 insertions(+), 9 deletions(-) diff --git a/.trinity/seals/t27mcp_T27mcpVisualOutput.json b/.trinity/seals/t27mcp_T27mcpVisualOutput.json index 53bca11e6d..a6e39b09ed 100644 --- a/.trinity/seals/t27mcp_T27mcpVisualOutput.json +++ b/.trinity/seals/t27mcp_T27mcpVisualOutput.json @@ -2,23 +2,23 @@ "built_by": "t27c-bootstrap@0.5.2+9448ab4be", "closure": [], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:076a0683dcc4e8487922bd6ff561422a1dfa707069e639fffaafea9d70ad8e2d", - "gen_hash_rust": "sha256:6267998500edaecfe3378c6ff7ed7482d1b0c239e1b8480fa697a8fd05b0d7b8", - "gen_hash_verilog": "sha256:241e62e3030919b4f540ff38480f8d726a4cc8301fbc30443369e7978adbbb1b", - "gen_hash_zig": "sha256:e7446390463513c4496f484f375ef59ae2f260172c8e46326f21008d84860411", + "gen_hash_c": "sha256:42e25ac4e6d33d72e11f45318478ef154bfc23b77daaa9a63e92d482718381c7", + "gen_hash_rust": "sha256:51c33ae8d38c5eebeaf38905c7577519df66fc057adee87a3d65c4eacf78818a", + "gen_hash_verilog": "sha256:a8af847801b01cfc44a3a88761714a6bf699fc91553ed72b5eaae7b40f60d5df", + "gen_hash_zig": "sha256:eb037cb00adb5f970d714f5c6a4d182289f326d3f8e890c124ebc5c3ef571b19", "module": "T27mcpVisualOutput", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-18T15:31:37Z", + "sealed_at": "2026-10-18T17:13:49Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:06c1c5e73142e2f79fa5d7be037eace63c1f20597a6e33274ebf8e1b428b7584", + "spec_hash": "sha256:2c44da44a1f662108a7b3558e763ff86e89db4bc9f49fe749dd7617051343960", "spec_path": "specs/t27mcp/visual_output.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 9, - "total": 9, + "passed": 12, + "total": 12, "vacuous": 0 }, "toolchain": { diff --git a/specs/t27mcp/visual_output.t27 b/specs/t27mcp/visual_output.t27 index 95a2fd98a1..22ad5fda9d 100644 --- a/specs/t27mcp/visual_output.t27 +++ b/specs/t27mcp/visual_output.t27 @@ -25,6 +25,15 @@ ; readable when F * INLINE_W >= MIN_FONT_PX * W for its smallest text. A tool SVG is embedded as it is only ; when readable and at most INLINE_MAX_BYTES (the model re-emits it, so its bytes are output); otherwise it ; is redrawn at INLINE_W from t27f_outline rows. +; UI resource (MCP Apps, the extension UI_EXTENSION): a client says it renders MCP Apps by naming +; UI_EXTENSION in its initialize capabilities with UI_MIME among its mimeTypes. Only that client is +; HOST_MCP_APPS (client_host); a client that says nothing is served the file path, the floor every host +; reads, since the server cannot tell the desktop app from the CLI. On HOST_MCP_APPS t27f_view and +; t27f_graph add a ui:// resource to their tools/call result: the SVG between UI_PAGE[0..2], a page with +; no script and nothing fetched. Each tool names its URI (UI_URI_*) in its tools/list _meta.ui.resourceUri, +; and resources/read of that URI returns the tool's latest drawing, UI_EMPTY before the first. Other hosts +; get no resource: its bytes would reach the model as text. With no drawing there is nothing to carry, so +; HOST_MCP_APPS falls back to the markdown table. ; phi^2 + 1/phi^2 = 3 | TRINITY module T27mcpVisualOutput; @@ -68,6 +77,20 @@ pub const INLINE_W : u32 = 680; pub const MIN_FONT_PX : u32 = 11; pub const INLINE_MAX_BYTES : u32 = 16384; +/// The MCP Apps resource: the capability a client names, the page's mime type, one URI per t27f tool, +/// the page around the SVG (the SVG goes between [1] and [2], the title between [0] and [1]), and the +/// page served before a tool has drawn. +pub const UI_EXTENSION : str = "io.modelcontextprotocol/ui"; +pub const UI_MIME : str = "text/html;profile=mcp-app"; +pub const UI_URI_VIEW : str = "ui://t27f/view"; +pub const UI_URI_GRAPH : str = "ui://t27f/graph"; +pub const UI_PAGE : [3]str = [ + "", + "", + "" +]; +pub const UI_EMPTY : str = "t27fNo drawing yet: call the tool first."; + /// Where a tool SVG's drawing comes from. pub const SRC_EMBED : u32 = 0; pub const SRC_REDRAW : u32 = 1; @@ -127,7 +150,10 @@ pub fn answer_channel(host: u32, inline_tool: bool, svg: bool) -> u32 { if (svg) { return CH_FILE_AND_PANE; } return CH_MARKDOWN_TABLE; } - if (host == HOST_MCP_APPS) { return CH_UI_RESOURCE; } + if (host == HOST_MCP_APPS) { + if (svg) { return CH_UI_RESOURCE; } + return CH_MARKDOWN_TABLE; + } if (host == HOST_SITE) { return CH_LIVE_PLAYER; } if (host == HOST_TRI_CLAW) { if (svg) { return CH_IMAGE_URL; } @@ -137,6 +163,20 @@ pub fn answer_channel(host: u32, inline_tool: bool, svg: bool) -> u32 { return CH_MARKDOWN_TABLE; } +/// The host an MCP client is, from its initialize capabilities: `ui_extension`, it names UI_EXTENSION; +/// `ui_mime`, UI_MIME is among that extension's mimeTypes. Anything less is served the file path. +pub fn client_host(ui_extension: bool, ui_mime: bool) -> u32 { + if (ui_extension) { + if (ui_mime) { return HOST_MCP_APPS; } + } + return HOST_CLAUDE_CODE_CLI; +} + +/// Whether a t27f tool's tools/call result carries its drawing as a ui:// resource on `host`. +pub fn carries_ui_resource(host: u32, svg: bool) -> bool { + return answer_channel(host, false, svg) == CH_UI_RESOURCE; +} + /// Whether text of `font_px` in a drawing `view_w` wide is at least MIN_FONT_PX when shown INLINE_W wide. pub fn inline_readable(view_w: u32, font_px: u32) -> bool { if (view_w == 0) { return false; } @@ -204,6 +244,36 @@ test "answer_channel: with no inline tool or no drawing, a path or a markdown ta assert answer_channel(HOST_MCP_APPS, false, true) == CH_UI_RESOURCE; assert answer_channel(HOST_SITE, false, true) == CH_LIVE_PLAYER; assert answer_channel(HOST_COUNT, false, true) == CH_FILE_PATH; + assert answer_channel(HOST_MCP_APPS, false, false) == CH_MARKDOWN_TABLE; + assert answer_channel(HOST_MCP_APPS, true, false) == CH_MARKDOWN_TABLE; +} + +test "client_host: only a client naming the extension and its mime is an MCP Apps host" { + assert client_host(true, true) == HOST_MCP_APPS; + assert client_host(true, false) == HOST_CLAUDE_CODE_CLI; + assert client_host(false, true) == HOST_CLAUDE_CODE_CLI; + assert client_host(false, false) == HOST_CLAUDE_CODE_CLI; +} + +test "carries_ui_resource: an MCP Apps host gets the drawing as a ui:// resource" { + assert carries_ui_resource(HOST_MCP_APPS, true); + assert carries_ui_resource(client_host(true, true), true); + assert answer_channel(HOST_MCP_APPS, true, true) == CH_UI_RESOURCE; + assert UI_EXTENSION == "io.modelcontextprotocol/ui"; + assert UI_MIME == "text/html;profile=mcp-app"; + assert UI_URI_VIEW == "ui://t27f/view"; + assert UI_URI_GRAPH == "ui://t27f/graph"; + assert UI_PAGE[2] == ""; +} + +test "negative control: no resource without a drawing, or on a host that did not ask for one" { + assert carries_ui_resource(HOST_MCP_APPS, false) == false; + assert carries_ui_resource(client_host(true, false), true) == false; + assert carries_ui_resource(client_host(false, false), true) == false; + assert carries_ui_resource(HOST_CLAUDE_CODE_DESKTOP, true) == false; + assert carries_ui_resource(HOST_TRI_CLAW, true) == false; + assert carries_ui_resource(HOST_SITE, true) == false; + assert carries_ui_resource(HOST_COUNT, true) == false; } test "inline_readable: the measured drawings" {