The embedded view
show_schedule shows a live schedule inside the conversation. It is not an image or an export: it is PalmaPlan's Gantt chart, drawn by the same code as the screen. It is editable if your role allows it, and it keeps up when someone else writes.
show_schedule { schedule: "S/4HANA Migration", view: "gantt", critical_path: true }
Options:
view:gantt(the default),kanbanoragenda;filter: the syntax of the screen's Filter panel;zoom;- a date window:
fromandto; - the panel:
grid,timeline, or both; critical_pathandreference(the baseline version);editable: falsefor a read-only view.
The user's actual role is always the ceiling: editable never grants more than a role allows.
What the view is: a native MCP App
PalmaPlan serves an MCP App: the resource ui://palmaplan/planning, of type text/html;profile=mcp-app. It is a single page: its HTML, JavaScript and CSS are inline, and nothing loads from elsewhere. show_schedule, what_if and diff_versions point to it (_meta.ui.resourceUri). A host that renders MCP Apps shows it in the conversation, in an isolated frame. It opens inline, then full screen on request, in the host's theme colours (light or dark), language and time zone.
The view only talks to the host. Its CSP opens no domain: no connectDomains, no frameDomains, no resourceDomains. It reads and writes by calling the server's tools (tools/call), which the host relays under the connector's token or OAuth. So the view has no token of its own, no nested frame and no direct access to PalmaPlan. Three tools are reserved for it (_meta.ui.visibility: ["app"]); the host hides them from the model:
| Tool | What it returns |
|---|---|
get_schedule_page | the schedule in pages, each response under 120,000 characters (Claude caps a tool result at about 150,000) |
get_changes_since | the journal entries after a rank, to stay up to date |
get_schedule_absences | the agenda: the members' names and the dates of their absences, never the reason or the e-mail |
The three views, and the before/after
- The Gantt chart: inline, the useful window of the schedule and its occupied rows; full screen, the whole schedule, groups included.
- The kanban board (
view: "kanban"): the bars' actions, in three columns (to do, in progress, done). Each card names its bar. - The agenda (
view: "agenda"): the members' absences, day by day, without the reason. - The before/after:
what_if(a proposal) anddiff_versions(a comparison) show as a Gantt chart where the proposed state is drawn over the shadow of the current one. Then come the list of changes (the bar, the old and new date), the project end and the new absence risks. A proposal has two buttons, "Accept" and "Decline"; a comparison is read-only.
"Accept" applies exactly what the before/after shows, and nothing else.
- The button calls
apply_patchwith the simulation's commands and the rank it was made at (expected_rank). The API only writes if the schedule is still at that rank, and it decides inside the very transaction that writes. If someone wrote in between, nothing is written and the view says: "This proposal is out of date — ask for a new one." - If the simulation carries changes that the list and the chart cannot show (settings, a row's member, notes…), "Accept" is not offered. The
what_iftext and the view name these "changes not shown". - The view only accepts
apply_patch, on the schedule being shown.
Gestures
In the Gantt chart, when the role allows it:
- select a bar; Shift+→ moves it by a working week, → and ← by one unit;
- drag a bar, or pull its edge to resize it;
- change an action's status;
- Undo and Redo (Ctrl/Cmd+Z, Shift+Ctrl/Cmd+Z);
- delete, always after a confirmation in the view.
Each gesture calls an existing tool (move_periods, resize_period, set_action_status, undo…), as the agent would. So the journal, undo and real time are the screen's. A refused write (role, lock, conflict) shows in the view. A viewer gets a read-only view.
Live updates: the view rereads the journal through get_changes_since, every two seconds at first, then less and less often while nothing moves (up to fifteen seconds). It stops while the frame is hidden. Each show_schedule call mounts a new frame; an older view of the same schedule withdraws on its own and says a newer view is open further down.
Talking to the model
- The model context (
ui/update-model-context): the view tells the model what is selected (the bar's id and label), the visible window and the open view. So "move this bar by a week" targets the right bar. - "Ask Claude…" (
ui/message) writes into the conversation about the selected bar. - "Help" opens through the host (
ui/open-link): an isolated frame opens nothing by itself.
Negotiation: who sees what
An MCP Apps host announces the io.modelcontextprotocol/ui extension with the type text/html;profile=mcp-app. The server reads three routes, and the first that announces it is enough:
capabilities.extensions["io.modelcontextprotocol/ui"]atinitialize(the stable spec's route);capabilities.experimental["io.modelcontextprotocol/ui"](the older route);_meta["io.modelcontextprotocol/clientCapabilities"]on a request: the stateless protocol's route. The server's SDK (1.x) does not speak that protocol yet, but the route is read.
What depends on it:
| An MCP Apps host | A client without MCP Apps (Claude Code) | |
|---|---|---|
tools/list | the 101 tools; the host hides the three view-only tools from the model | the 98 model tools: the three view-only tools are not listed (nothing there would hide them from the model, and get_schedule_page returns up to 120,000 characters) |
show_schedule | the view; a text that sums it up; no link and no token | the text and the one-hour /embed link |
what_if, diff_versions | the before/after, and their text | their text |
Calling a view-only tool still works for everyone. Visibility is not a permission boundary: the same data reads through palmaplan://schedule/{id}/json, under the same rights. And a host that rendered the view without announcing the extension does not find it empty.
What Claude Code shows: text
Claude Code does not render MCP Apps. There, show_schedule returns a text that stands on its own: the name, the period, the number of groups, rows, bars and milestones, the risks (violated links, bars at risk of absence) and the one-hour link:
Planning « S/4HANA Migration » — du 2026-11-02 au 2026-12-18 ; 2 groupes, 6 lignes, 6 blocs, 1 jalon.
Risques : 1 bloc à risque d absence.
Cet hôte n'affiche pas les vues MCP Apps : cette adresse ouvre le planning, valable une heure, en lecture seule : http://127.0.0.1:4785/embed/<id>?t=…
(The tools' texts are in French.) Its structuredContent is compact: { schedule_id, name, view, options, rank, read_only, shown_at, url, expires_at }. It never carries the model, because Claude Code gives it to the model instead of the text. A test checks all 98 model tools the same way: each returns a useful text (a result, or a refusal that says what to fix) and a compact structuredContent.
The /embed link, for hosts without MCP Apps
The link opens PalmaPlan's screen, reduced to the schedule, in a browser or in an iframe on another site. It carries a signed token valid for one hour, capped to the schedule and to the actual role. This link:
- is read-only unless
editable: trueis asked for; - under its token, the member list has no e-mail and the agenda no reason;
- does not renew itself: it expires, and the view says so.
It is not a named token: it does not appear under Settings › Connections and agents › Connected applications (see Tokens and scopes).
Limits
- Size: a schedule is read in pages of at most 120,000 characters. A text longer than a whole page (an outsized label) arrives cut, and the view says so in one line.
- Live updates use polling, not the WebSocket channel: a few seconds of delay, not the screen's instant.
- No stable origin (
ui.domain): nothing is kept in the view from one conversation to the next. - Mobile: the host limits the inline height, and full screen suits editing better. At 390 px, "Help" and "Full screen" fold to their icon.
- Writes from the view go through the same tools as the agent: a write the agent cannot make, the view cannot make either.
What is proven, and what is not
Proven, in the repository:
- by the test host (
scripts/hote-mcp-apps.ts), built on the MCP Apps SDK'sAppBridgeand driven by Chromium. It reproduces what Claude's public documentation says about its hosts: two frames on two distinct origins, the CSP built from_meta.ui.cspand set as a header,frameDomainsignored, tool visibility enforced; - by the sprint 24 acceptance run (twenty points): every protocol message played, no CSP violation, no request from the view to any origin; a gesture in the view seen on the web screen and the other way round; the model context; the before/after and "Accept", including the out-of-date proposal; negotiation (the 98 model tools without the capability, the 101 with it); a view-only tool refused to the model; a tool beyond the role refused to the view; Claude Code as text only.
Not proven: no real host has displayed this view yet. Not Claude Desktop, not claude.ai, not any other MCP Apps host. The test host follows the documentation, not Claude's code: its exact CSP, the way it announces the MCP Apps capability at initialize, its mobile WebView may differ. In particular, if a real host displays the view without announcing the extension, the model there will only see the 98 model tools, and the view will keep working, since view-only calls are still served. The steps for the real trial are in Connecting Claude Desktop, section "Trying the view in Claude Desktop". Until it has been run, treat the view as proven at the protocol level, not as proven in Claude.