# 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), `kanban` or `agenda`;
- `filter`: the syntax of the screen's Filter panel;
- `zoom`;
- a date window: `from` and `to`;
- the panel: `grid`, `timeline`, or both;
- `critical_path` and `reference` (the baseline version);
- `editable: false` for 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](https://modelcontextprotocol.io): 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) and `diff_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_patch` with 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_if` text 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:

1. `capabilities.extensions["io.modelcontextprotocol/ui"]` at `initialize` (the stable
   spec's route);
2. `capabilities.experimental["io.modelcontextprotocol/ui"]` (the older route);
3. `_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: true` is 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](jetons-et-portees.md)).

## 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's `AppBridge` and
  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.csp` and set as a
  header, `frameDomains` ignored, 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](claude-desktop.md), 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**.
