From a2adbd1703067644173e9f680905c39c30641b8c Mon Sep 17 00:00:00 2001 From: Marty Bradley Date: Fri, 23 Oct 2026 11:10:23 -0400 Subject: [PATCH 1/3] docs(panels): brand guide and the opportunities panel plan panels/BRAND.md applies the Accelerated Innovation Brand Book V01 to MCP App panels. The rule is the host's look plus brand accents and one small mark: - accent colours chosen per theme from measured WCAG contrast; - the host's font, required because the MCP Apps default CSP blocks fonts; - left-aligned type, and status never shown by colour alone; - the official mark, unaltered; - source notes on three brand book discrepancies, and the PR checklist discovery-engine feature 27 (N12) requires. docs/plans/2026-10-23-opportunities-panel.md plans the first panel: panels/ beside viewers/ and why; behaviour; the MCP Apps 2026-01-26 bridge; the contract test on the built file; increments 2-6 through release, the engine bump PR and host verification; open questions. No plugin files change, so no version bump. Co-Authored-By: Claude Opus 5.5 --- docs/plans/2026-10-23-opportunities-panel.md | 157 +++++++++++++++++ panels/BRAND.md | 174 +++++++++++++++++++ 2 files changed, 331 insertions(+) create mode 100644 docs/plans/2026-10-23-opportunities-panel.md create mode 100644 panels/BRAND.md diff --git a/docs/plans/2026-10-23-opportunities-panel.md b/docs/plans/2026-10-23-opportunities-panel.md new file mode 100644 index 0000000..d2f19b6 --- /dev/null +++ b/docs/plans/2026-10-23-opportunities-panel.md @@ -0,0 +1,157 @@ +# Opportunities panel (the first MCP App panel) + +**Updated · 2026-10-23.** Owner: AIPOS (Marty Bradley). Planned; nothing is built here yet. +The engine side is complete: discovery-engine feature 27 (`27_engine_mcp_opportunities_panel`, +PRs #151–#156). The engine binds this panel to `list_opportunities` and serves it as +`ui://aipos/opportunities` once a release is pinned there. This repository authors, brands, +tests and releases the panel. + +## Purpose and ownership + +When a PM asks for opportunities in an MCP-Apps-capable host (Claude, Claude Desktop), the host +renders an interactive panel next to the `list_opportunities` result. The PM can filter the +ranked list, open a problem's detail and evidence references, and hand a selection back to the +conversation. Hosts without MCP Apps keep exactly today's JSON answer. + +| Repository | Owns | Shared contract | +|---|---|---| +| discovery-engine | Binding, serving and verifying the pinned file; `/health` status; the allowed tool set | `docs/backend/api-contract/ui_handoff.md`, "MCP App panels"; feature 27 `nfrs.md` | +| aipos-plugins (here) | Panel source, brand (`panels/BRAND.md`), UI tests, the contract test on the built file, releases | One HTML file per release, with version and SHA-256 | +| MCP host | Rendering in a sandboxed iframe; proxying the panel's tool calls with the user's token | MCP Apps spec, version 2026-01-26 | + +Authoritative engine package: +https://github.com/Accelerated-Innovation/discovery-engine/tree/main/features/27_engine_mcp_opportunities_panel + +## Where it lives: `panels/`, beside `viewers/` + +`viewers/` holds offline snapshots: HTML a skill writes from captured data, shipped inside the +plugin under `plugins/aipos/skills/*/scripts/assets/` and released by a plugin version bump. +A panel differs in four ways: +- **Who ships it:** the engine serves it, not the plugin. +- **Data:** it reads live through the host instead of baking data in. +- **Release:** a tagged file pinned by a bump PR in discovery-engine. +- **Gates:** it carries the engine's extra ones (size, no external requests, allowed tool names). + +Keeping it in a separate top-level folder keeps the two release paths from being confused. + +``` +panels/ + BRAND.md # how every panel carries the brand (done in this PR) + opportunities/ + template.html app.mjs model.mjs style.css # source, dependency-free like viewers/portfolio + bridge.mjs # the MCP Apps host bridge (see "Host bridge") + build.py # inlines everything into one HTML file; --check for freshness + dist/opportunities.html # committed build output, reviewed as generated code + model.test.mjs check-page.mjs fixtures/ +tests/test_panel_contract.py # the engine contract, run on dist/ in pytest +``` + +`model.mjs` is pure: it shapes tool results into rows and filters, so it can be shared with +`viewers/` later. Nothing under `panels/` is packaged into the plugin, so a panel change needs +no plugin version bump until the "Afterwards" step below. + +## Behaviour + +From feature 27's user flows. The panel only reads; anything that changes something goes back to +the conversation. + +1. **Open.** The model calls `list_opportunities`, and the host renders the panel with that call's + input and result. Show the ranked list: problem title, composite score, score components, and + active work-item links. It is one page of the result (`offset`, `limit`, `total`); offer "more" + by calling `list_opportunities` with the next offset. +2. **Filter and sort** in the panel, over the rows it holds. Say plainly when a filter covers only + the loaded page. +3. **Drill in.** On selection, call `get_problem` and `list_evidence` for that `problem_id`, and + show personas, lineage, and evidence **as provenance references** (source, record link, study + measurement when present). **No evidence text:** `get_evidence_text` is not allowed, and the + engine refuses to serve a panel that names it. +4. **Hand back.** Selected opportunities plus an action ("draft Aha! records", "package for + gate 1") become a `ui/message` to the conversation. The skill and its confirmation step take + it from there. No write tool is ever called from the panel. +5. **Fallbacks.** Show empty states for an empty list (no scoring run yet), errors for a refused + or failed call (show the engine's code: `INSUFFICIENT_SCOPE`, `INVALID_TOKEN`), and a stale + label (capture time, past 24 hours) per `plugins/aipos/references/panel-contract.md`. + Provenance on the panel: source mode `connected`, the server name, the read time, and + coverage (rows shown of `total`). + +## Host bridge (MCP Apps spec 2026-01-26) + +The panel speaks JSON-RPC over `postMessage` with the host: +- `ui/initialize`, then `ui/notifications/initialized`, for the handshake; +- `ui/notifications/tool-input` and `ui/notifications/tool-result` deliver the + `list_opportunities` call that opened it; +- `tools/call` reaches `list_opportunities`, `get_problem` and `list_evidence` (the engine leaves + their `visibility` at the default, model and app); +- `ui/message` hands back to the conversation; +- `ui/notifications/size-changed` asks for height; +- `ui/notifications/host-context-changed` follows theme changes; +- `ui/open-link` opens record links. + +Re-check these names against the spec version the target host implements before building. + +**Decision for increment 2:** a hand-written `bridge.mjs` covering only the messages above, or the +official SDK `@modelcontextprotocol/ext-apps` (2.0.3 at writing) bundled in. Hand-written keeps the +file small and dependency-free and makes the tool-name scan trivial to reason about. The SDK +tracks the spec. Recommendation: hand-written, unless the target host needs negotiation the SDK +handles. Either way, the contract test below decides. + +## Brand + +`panels/BRAND.md`, from the Accelerated Innovation Brand Book V01. In short: +- the host's surfaces and font; +- purple accent in light mode, light blue in dark; +- one small official mark; +- left-aligned type; +- status never by colour alone. + +Every panel PR carries its checklist and light and dark screenshots, reviewed by Marty Bradley. + +## The contract test (on the built file) + +`tests/test_panel_contract.py`, in pytest, so the `validate.yml` pytest job runs it on every PR. +This is the primary control; the engine's load-time scan is only a tripwire. +- `dist/opportunities.html` is **at most 256,000 bytes** and valid UTF-8. +- **No external requests:** no `http(s)://` in `src`, `href` (except record links opened through + `ui/open-link`), `@import`, `url(…)` or `fetch(`; no ``; no web fonts. +- **Tool names:** names `list_opportunities`, `get_problem` and `list_evidence`, and **none** of + the engine's other tools. That means no other tool name as a whole identifier anywhere in the + file, comments and bundled code included. The forbidden list mirrors discovery-engine's + `src/engine/api/mcp/panels.py` `ENGINE_TOOL_NAMES`; the test cites that file so the two are + updated together. +- `build.py --check` passes: the committed `dist/` is what the source builds. + +## Increments and acceptance + +| Increment | Result | Completion evidence | +|---|---|---| +| 1 — Brand and plan | `panels/BRAND.md`; this plan | Reviewed and merged | +| 2 — Model and fixtures | `model.mjs` (rows, filters, sort, paging state, provenance), fixtures from discovery-engine's published samples (`opportunities.json`, `problem-detail.json`, `problem-evidence.json`), the bridge decision | `node --test panels/opportunities/model.test.mjs`; fixtures cite their source paths and SHA-256 | +| 3 — Page, bridge, build | `template.html`, `app.mjs`, `style.css`, `bridge.mjs`, `build.py` producing `dist/opportunities.html`; a mock-host page for local checks | `check-page.mjs` drives the mock host with Playwright (reusing the journey viewer's pinned install): open, filter, drill in, hand back, error and empty states, keyboard only; light and dark screenshots | +| 4 — Contract test and CI | `tests/test_panel_contract.py`; `build.py --check` and `node --test` wired into `validate.yml` | Each check proven able to fail (a planted forbidden name, an external URL, an oversized file) | +| 5 — Release | Tag `panels-opportunities-v`; `gh release` with `opportunities.html` and its SHA-256 in the notes | Release published; SHA-256 recomputed from the downloaded asset | +| 6 — Pin and verify in a host | discovery-engine bump PR (its `DEPLOYMENT.md` part F1); the engine run with `ENGINE_MCP_PANELS_ENABLED=true` (stdio for Claude Desktop, or the hosted engine via F2) | `/health` shows `bound` at the release version; the panel renders in Claude Desktop in light and dark; drill-in and hand-back work against the real engine | + +**Afterwards:** update the "Native in-chat panel (MCP App): Not available" row in +`plugins/aipos/references/panel-contract.md`, which is a plugin change and needs a version bump. +Tell the skills that call `list_opportunities` that a panel may be showing. + +## For the demo + +Increments 2–6 must land, in order. The engine side needs nothing more than the pin (6) and the +switch. For a local demo in Claude Desktop, the engine's server entry in +`~/Library/Application Support/Claude/claude_desktop_config.json` needs these in its `env`: +- `ENGINE_MCP_PANELS_ENABLED=true`; +- `ENGINE_MCP_TOKEN`; +- `JWT_SECRET_KEY`, equal to the secret the token was minted with. The stdio server reads `.env` + only from its working directory. + +## Open questions + +1. The bridge implementation: hand-written, or the SDK (decide in increment 2). +2. The official SVG artwork for the mark (`BRAND.md` "Open items"). The panel ships without a mark + until it arrives. +3. Brand book discrepancies for the brand owner (`BRAND.md` "Source notes"): the secondary blue + and yellow RGB values against their hex codes, and the wordmark minimum size. +4. Whether hand-back actions ship in v1 or the first release is read-only browsing. +5. Which host versions to verify against: Claude Desktop is the demo host; check whether Claude + Code renders MCP App panels before relying on it. diff --git a/panels/BRAND.md b/panels/BRAND.md new file mode 100644 index 0000000..a593d75 --- /dev/null +++ b/panels/BRAND.md @@ -0,0 +1,174 @@ +# Panel brand guide + +How MCP App panels built in `panels/` carry the Accelerated Innovation brand. Source: the +*Accelerated Innovation Brand Book*, V01 (©2023), applied to an interactive view rendered inside +someone else's chat. Every panel PR is reviewed against the checklist at the end. + +**The rule:** a panel looks like it belongs to the host, light or dark, and is recognisably ours +through **accent colour and one small mark**. It never imposes a full brand canvas (gradient +backgrounds, brand-coloured surfaces, custom fonts) on the host's chat. Decided in +discovery-engine feature 27 (nfrs D9–D11); the brand book supports it ("use the most legible +version … for the available background", p. 12; system fonts when custom fonts are not +realistic, p. 24). + +## Hard constraints from the engine (not brand choices) + +These come from the release contract in discovery-engine's +`docs/backend/api-contract/ui_handoff.md`, "MCP App panels". A panel that breaks one is not +bound by the engine. + +- **No external requests:** no web fonts, CDN scripts, remote images or `fetch` to anything but + the host bridge. The resource declares no CSP origins, so the host applies the spec's default + policy: + - scripts and styles inline only; + - images `self` and `data:`; + - no `connect-src`; + - nothing else, fonts included. +- One self-contained HTML file, **at most 256,000 bytes**, valid UTF-8. +- Names no engine tool other than the ones the panel may call. For the opportunities panel those + are `list_opportunities`, `get_problem` and `list_evidence`. + +## Colour + +Brand palette (brand book pp. 20–22). Use **hex values**; see "Source notes" for two entries +whose RGB and hex disagree. + +| Role | Name | Hex | +|---|---|---| +| Primary | Dark purple | `#19004f` | +| Primary | Purple | `#5865e7` | +| Primary | Light blue | `#0abeef` | +| Primary | Magenta | `#821db0` | +| Secondary | Blue | `#1a4b84` | +| Secondary | Red | `#af1e44` | +| Secondary | Green | `#6fa113` | +| Secondary | Yellow | `#d1b70d` | +| Neutral | Accelerated Innovation White | `#f2f2f2` | +| Neutral | 90% Black | `#1a1a1a` | + +### Surfaces belong to the host + +- Use the host's theme. The MCP Apps host context carries `theme` (`light` or `dark`) and + CSS variables in `styles.variables` (MCP Apps spec 2026-01-26, "Theming"): + `--color-background-primary`, `--color-text-primary`, `--color-border-primary`, + `--color-ring-primary`, `--font-sans` and their siblings. Read them at `ui/initialize`, and + again on `ui/notifications/host-context-changed`. +- Hosts may omit any variable. Fall back to the brand neutrals: `#f2f2f2` or white in light + mode, `#1a1a1a` in dark. Never true black, and never true white for large dark-mode surfaces + (p. 22). +- No brand-coloured backgrounds and no gradient backgrounds behind content. + +### Accents change with the theme + +WCAG 2.1 contrast, measured against the surfaces a panel actually sits on. AA needs **4.5:1 for +text**, and **3:1 for large text (≥ 24 px, or ≥ 18.7 px bold) and for UI parts** such as focus +rings, borders and icons. + +| Colour | on `#ffffff` | on `#f2f2f2` | on `#1a1a1a` | Use | +|---|---|---|---|---| +| Dark purple `#19004f` | 18.19 | 16.25 | 1.05 | **Light:** headings, emphasis text. **Dark:** never | +| Purple `#5865e7` | 4.74 | 4.23 | 3.67 | **Light:** links and accent text on white only; UI parts on any light surface. **Dark:** UI parts and large text only | +| Light blue `#0abeef` | 2.18 | 1.95 | 7.98 | **Light:** decoration only, never text or a sole indicator. **Dark:** the accent for links and text | +| Magenta `#821db0` | 7.63 | 6.82 | 2.28 | **Light:** secondary accent text. **Dark:** decoration only | + +So the panel's accent is **purple in light mode and light blue in dark mode**, with dark purple +for light-mode headings. Define them once as CSS custom properties per theme; never hard-code +a brand colour in a component. + +### Status colours + +The secondary colours may mark status (p. 21: "to highlight and add details"), in smaller +proportions than the primaries. **Status never relies on colour alone**: pair it with a word or +an icon. As text, green (3.10 on white) and yellow (2.00 on white) fail on light surfaces, and +red (2.56) and blue (1.98) fail on dark. Use them as fills or markers beside a text label, not as +the label. + +### Gradient + +The brand's linear gradient (`#0abeef` → `#5865e7` → `#19004f`, 35°, p. 32) may appear only as +a **thin decorative rule** (≤ 4 px), for example under the panel header. Never behind text: +contrast varies across it. + +## Type + +- **Font: the host's, by necessity.** The brand book asks for Poppins (p. 24), but a panel + cannot load it: + - a web font is an external request, which the engine contract forbids; + - the spec's default sandbox policy has no `font-src`, so it falls back to + `default-src 'none'` and blocks even an inlined `data:` font. + + The brand book's own exception covers this: system sans-serif when custom fonts are not + realistic. Set `font-family: var(--font-sans, system-ui, sans-serif)`. +- **Hierarchy through weight and size** (pp. 25–26): up to four weights (300, 400, 500, 700). + When pairing, skip a weight and roughly double the size. Avoid weights the host font lacks. +- **Left-aligned, ragged right.** No centred or justified multi-line text (p. 27, "no + exceptions"). Single-line numbers in table cells may be right-aligned for comparison. +- **Line length 45–70 characters** for prose (p. 26). Tables and lists are exempt. +- No stroke, outline or drop shadow on text. Text runs at 0°, or 90° reading up (p. 27). +- Never stretch or squash type; use a weight instead. + +## The mark + +- **One mark per panel, and small.** Use the **wordmark lockup**, which the brand book designs + for small spaces (p. 14). Minimum height **25 px** (see "Source notes"). Place it in the header + or footer, not over content. +- Use the **icon-only lockup** only where the brand name is visible beside it (p. 13), at a + minimum of **50 px** high. +- Colour variant by theme (p. 12): **full colour on light** surfaces; **white / AI White + (`#f2f2f2`) one-colour on dark** surfaces. +- Clear space: at least 1½ × the height of the "i" tittle on every side (p. 16). +- **Inline the official SVG artwork, unaltered:** no redrawing, recolouring, stretching, + effects or drop shadows (pp. 10, 18). The logo is "locked artwork". The SVG comes from the + brand owner's source files, not traced from the PDF (see "Open items"). +- Never place the mark on a busy or high-contrast background (p. 18). + +## Icons + +Thin outlines and geometric shapes (p. 30). Keep them simple, never partially cut off, and never +used as or inside the logo. Inline them as SVG; no icon fonts. An icon that conveys state needs a +text label or an accessible name. + +## Layout + +- Generous negative space; group related elements tightly and separate groups (p. 26). +- Brand "shapes" (crops of the logo icon used as decoration, p. 31) are not used in panels: they + compete with the data. +- Imagery (p. 29): none. A panel shows data, not photography. + +## Source notes (discrepancies in the brand book, V01) + +1. **Secondary Blue:** hex `#1a4b84` vs RGB 30, 64, 175 (`#1e40af`). This guide uses the hex. +2. **Secondary Yellow:** hex `#d1b70d` vs RGB 209, 193, 13 (`#d1c10d`). This guide uses the hex. +3. **Wordmark minimum:** 18 px (p. 14) vs 25 px (pp. 14 caption and 15). This guide uses 25 px. +4. Page 5 ("Our Story") is placeholder text, and the closing page carries another company's mark + (template residue). Neither affects panels. + +The brand owner should confirm items 1–3, and correct this guide if the RGB values are the +intended ones. + +## Open items + +- **Official SVG artwork** for the wordmark lockup and the icon, in full colour and one colour. + Until it is supplied, panels ship without a mark rather than with a traced one. +- **Poppins in panels is not possible** while panels declare no CSP origins (see Type). It + would take an engine-side change to declare a font origin, which ADR-AIG-039 rules out. + +## Panel PR checklist + +Copy into every PR that changes a panel. The reviewer is Marty Bradley (discovery-engine +feature 27, N12). + +- [ ] Light and dark screenshots attached, in a host or the panel's check page, at the default + panel size and a narrow width +- [ ] Surfaces, text and font come from the host theme, with fallbacks as above; no brand + background, gradient behind text, or web font +- [ ] Accent is purple in light mode and light blue in dark; dark purple only for light-mode + headings +- [ ] All text meets 4.5:1 (3:1 large); focus rings and indicators meet 3:1 in both themes +- [ ] Status is never colour alone +- [ ] One mark: the official, unaltered SVG at or above its minimum size, correct variant for the + theme, with clear space (or no mark while the artwork is outstanding) +- [ ] Left-aligned, ragged right; no centred or justified multi-line text; no text effects +- [ ] Icons are thin-outline inline SVGs with accessible names where they convey meaning +- [ ] The built file passes the panel contract test: size, UTF-8, no external requests, and only + the allowed tool names From dbd175c51f96af21bc1f504cc3922a47fb51ca0a Mon Sep 17 00:00:00 2001 From: Marty Bradley Date: Fri, 23 Oct 2026 11:33:20 -0400 Subject: [PATCH 2/3] docs(panels): address the CodeRabbit review of #97 BRAND.md: - the inline-only rule is attributed to the engine, and the MCP Apps default CSP is described as it is (self and inline scripts and styles, data: images, no connections, no fonts); - partial host-context updates are merged, never replacing the current context; - accents are checked at runtime against the resolved host surface, falling back to host colours; - status uses the host's semantic colours first. Brand secondaries that miss 3:1 on a surface are decorative only there, with a per-surface table. Plan: hand-back in v1 becomes decision Q4, taken before increment 2 (it defines the ui/message payload if yes), and increment 3 tests hand-back only then. Increment 4 names the new validate.yml steps (build.py --check and the panel's node tests). Co-Authored-By: Claude Opus 5.5 --- docs/plans/2026-10-23-opportunities-panel.md | 24 +++++--- panels/BRAND.md | 60 +++++++++++++++----- 2 files changed, 62 insertions(+), 22 deletions(-) diff --git a/docs/plans/2026-10-23-opportunities-panel.md b/docs/plans/2026-10-23-opportunities-panel.md index d2f19b6..cac6a13 100644 --- a/docs/plans/2026-10-23-opportunities-panel.md +++ b/docs/plans/2026-10-23-opportunities-panel.md @@ -65,9 +65,15 @@ the conversation. show personas, lineage, and evidence **as provenance references** (source, record link, study measurement when present). **No evidence text:** `get_evidence_text` is not allowed, and the engine refuses to serve a panel that names it. -4. **Hand back.** Selected opportunities plus an action ("draft Aha! records", "package for - gate 1") become a `ui/message` to the conversation. The skill and its confirmation step take - it from there. No write tool is ever called from the panel. +4. **Hand back** (in v1 only if decision Q4 says so; see "Open questions"). Selected + opportunities plus an action ("draft Aha! records", "package for gate 1") become a + `ui/message` to the conversation. The skill and its confirmation step take it from there. If + it is in v1, increment 2 defines the payload and the consumer contract: + - the message text the model reads; + - the `problem_id`s it carries; + - which skill picks it up. + + No write tool is ever called from the panel. 5. **Fallbacks.** Show empty states for an empty list (no scoring run yet), errors for a refused or failed call (show the engine's code: `INSUFFICIENT_SCOPE`, `INVALID_TOKEN`), and a stale label (capture time, past 24 hours) per `plugins/aipos/references/panel-contract.md`. @@ -118,7 +124,9 @@ This is the primary control; the engine's load-time scan is only a tripwire. file, comments and bundled code included. The forbidden list mirrors discovery-engine's `src/engine/api/mcp/panels.py` `ENGINE_TOOL_NAMES`; the test cites that file so the two are updated together. -- `build.py --check` passes: the committed `dist/` is what the source builds. +- `build.py --check` passes: the committed `dist/` is what the source builds. This and the + model's `node --test` need their own `validate.yml` steps, added in increment 4; the existing + Node steps cover only `viewers/`. ## Increments and acceptance @@ -126,8 +134,8 @@ This is the primary control; the engine's load-time scan is only a tripwire. |---|---|---| | 1 — Brand and plan | `panels/BRAND.md`; this plan | Reviewed and merged | | 2 — Model and fixtures | `model.mjs` (rows, filters, sort, paging state, provenance), fixtures from discovery-engine's published samples (`opportunities.json`, `problem-detail.json`, `problem-evidence.json`), the bridge decision | `node --test panels/opportunities/model.test.mjs`; fixtures cite their source paths and SHA-256 | -| 3 — Page, bridge, build | `template.html`, `app.mjs`, `style.css`, `bridge.mjs`, `build.py` producing `dist/opportunities.html`; a mock-host page for local checks | `check-page.mjs` drives the mock host with Playwright (reusing the journey viewer's pinned install): open, filter, drill in, hand back, error and empty states, keyboard only; light and dark screenshots | -| 4 — Contract test and CI | `tests/test_panel_contract.py`; `build.py --check` and `node --test` wired into `validate.yml` | Each check proven able to fail (a planted forbidden name, an external URL, an oversized file) | +| 3 — Page, bridge, build | `template.html`, `app.mjs`, `style.css`, `bridge.mjs`, `build.py` producing `dist/opportunities.html`; a mock-host page for local checks | `check-page.mjs` drives the mock host with Playwright (reusing the journey viewer's pinned install): open, filter, drill in, error and empty states, a partial host-context update, keyboard only; hand back **only if Q4 put it in v1**, asserting the exact `ui/message` payload; light and dark screenshots | +| 4 — Contract test and CI | `tests/test_panel_contract.py`; **new `validate.yml` steps** for `python panels/opportunities/build.py --check` and `node --test panels/opportunities/model.test.mjs`. Today's Node steps test only the viewers | Each check proven able to fail (a planted forbidden name, an external URL, an oversized file, a stale `dist/`); the increment is not done until the new steps run on a PR | | 5 — Release | Tag `panels-opportunities-v`; `gh release` with `opportunities.html` and its SHA-256 in the notes | Release published; SHA-256 recomputed from the downloaded asset | | 6 — Pin and verify in a host | discovery-engine bump PR (its `DEPLOYMENT.md` part F1); the engine run with `ENGINE_MCP_PANELS_ENABLED=true` (stdio for Claude Desktop, or the hosted engine via F2) | `/health` shows `bound` at the release version; the panel renders in Claude Desktop in light and dark; drill-in and hand-back work against the real engine | @@ -152,6 +160,8 @@ switch. For a local demo in Claude Desktop, the engine's server entry in until it arrives. 3. Brand book discrepancies for the brand owner (`BRAND.md` "Source notes"): the secondary blue and yellow RGB values against their hex codes, and the wordmark minimum size. -4. Whether hand-back actions ship in v1 or the first release is read-only browsing. +4. **Hand-back in v1, or read-only browsing first? Decide before increment 2**, because + increment 3's tests depend on it. If yes, increment 2 also defines the `ui/message` payload + and the receiving skill's contract. 5. Which host versions to verify against: Claude Desktop is the demo host; check whether Claude Code renders MCP App panels before relying on it. diff --git a/panels/BRAND.md b/panels/BRAND.md index a593d75..c9b3e9c 100644 --- a/panels/BRAND.md +++ b/panels/BRAND.md @@ -18,13 +18,14 @@ These come from the release contract in discovery-engine's bound by the engine. - **No external requests:** no web fonts, CDN scripts, remote images or `fetch` to anything but - the host bridge. The resource declares no CSP origins, so the host applies the spec's default - policy: - - scripts and styles inline only; - - images `self` and `data:`; - - no `connect-src`; - - nothing else, fonts included. -- One self-contained HTML file, **at most 256,000 bytes**, valid UTF-8. + the host bridge. This is the engine's rule. +- **One self-contained HTML file, at most 256,000 bytes, valid UTF-8**, with every script and + style inline. This is also the engine's rule: it serves one pinned file and nothing beside it. +- **The CSP the host applies.** The resource declares no CSP origins, so hosts apply the MCP Apps + default policy (spec 2026-01-26). That allows scripts and styles from `self` and inline, images + from `self` and `data:`, no connections (`connect-src 'none'`), and no fonts (`font-src` is + absent, so `default-src 'none'` applies). The inline-only rule comes from the engine, not from + this policy. - Names no engine tool other than the ones the panel may call. For the opportunities panel those are `list_opportunities`, `get_problem` and `list_evidence`. @@ -53,6 +54,9 @@ whose RGB and hex disagree. `--color-background-primary`, `--color-text-primary`, `--color-border-primary`, `--color-ring-primary`, `--font-sans` and their siblings. Read them at `ui/initialize`, and again on `ui/notifications/host-context-changed`. +- `ui/notifications/host-context-changed` carries a **partial** context, for example a theme + change without `styles.variables`. **Merge each update into the panel's current host context**; + never replace it, or a theme-only update discards the surface and font variables. - Hosts may omit any variable. Fall back to the brand neutrals: `#f2f2f2` or white in light mode, `#1a1a1a` in dark. Never true black, and never true white for large dark-mode surfaces (p. 22). @@ -75,13 +79,35 @@ So the panel's accent is **purple in light mode and light blue in dark mode**, w for light-mode headings. Define them once as CSS custom properties per theme; never hard-code a brand colour in a component. +**Check against the surface actually resolved, not the theme name.** The table assumes the +fallback surfaces, but a host's `--color-background-primary` may be another shade. Each time the +host context changes, the panel computes the contrast of each accent against the resolved +background, using a small luminance function in `model.mjs` with its own unit tests. If an accent +falls below its threshold (4.5:1 for text, 3:1 for UI parts), that use falls back to the host's +own `--color-text-primary` or `--color-ring-primary`. The brand colour then stays decorative +only. + ### Status colours -The secondary colours may mark status (p. 21: "to highlight and add details"), in smaller -proportions than the primaries. **Status never relies on colour alone**: pair it with a word or -an icon. As text, green (3.10 on white) and yellow (2.00 on white) fail on light surfaces, and -red (2.56) and blue (1.98) fail on dark. Use them as fills or markers beside a text label, not as -the label. +**Status never relies on colour alone**: pair it with a word or an icon. A status **marker** +(dot, bar, icon) is a UI part and must still meet **3:1** against its surface. + +1. **Prefer the host's semantic status colours:** `--color-text-danger`, `--color-text-success`, + `--color-text-warning`, `--color-text-info`, and their `--color-background-*` and + `--color-border-*` siblings. They are tuned to the host's surfaces. +2. **Fall back to the brand secondaries** (p. 21, "to highlight and add details") only where they + pass on the resolved surface: + +| Marker | on `#ffffff` | on `#f2f2f2` | on `#1a1a1a` | +|---|---|---|---| +| Red `#af1e44` | 6.79 ✓ | 6.07 ✓ | 2.56 ✗ | +| Blue `#1a4b84` | 8.81 ✓ | 7.87 ✓ | 1.98 ✗ | +| Green `#6fa113` | 3.10 ✓ | 2.77 ✗ | 5.62 ✓ | +| Yellow `#d1b70d` | 2.00 ✗ | 1.79 ✗ | 8.69 ✓ | + +A colour marked ✗ on a surface is **decorative only** there: never a marker, never text. The +runtime check above applies to markers too. As text (4.5:1), only red and blue on light surfaces, +and yellow and green on dark, are usable. ### Gradient @@ -163,9 +189,13 @@ feature 27, N12). - [ ] Surfaces, text and font come from the host theme, with fallbacks as above; no brand background, gradient behind text, or web font - [ ] Accent is purple in light mode and light blue in dark; dark purple only for light-mode - headings -- [ ] All text meets 4.5:1 (3:1 large); focus rings and indicators meet 3:1 in both themes -- [ ] Status is never colour alone + headings; each use is checked against the resolved host surface and falls back to the host + colour when it fails +- [ ] All text meets 4.5:1 (3:1 large); focus rings, markers and indicators meet 3:1 in both + themes +- [ ] Status uses the host's semantic colours first, is never colour alone, and uses no brand + secondary marked ✗ for the surface +- [ ] Host-context updates are merged into the current context, never replacing it - [ ] One mark: the official, unaltered SVG at or above its minimum size, correct variant for the theme, with clear space (or no mark while the artwork is outstanding) - [ ] Left-aligned, ragged right; no centred or justified multi-line text; no text effects From 19ee07c2b56879eae74553cab621470774826fa8 Mon Sep 17 00:00:00 2001 From: Marty Bradley Date: Fri, 23 Oct 2026 11:54:35 -0400 Subject: [PATCH 3/3] docs(panels): select the MCP Apps SDK for the opportunities panel Record the decision to build the panel's bridge on the official @modelcontextprotocol/ext-apps SDK (exact 2.0.3) behind a narrow bridge.mjs adapter, bundled locally with a locked dependency tree and a build.mjs (esbuild) in place of build.py. Measured SDK bundles exceed the engine's 256,000-byte ceiling, so the plan proposes a 1 MiB limit as a pending cross-repository contract change that discovery-engine must land before the panel is pinned. Increment 2 now includes an early Claude Desktop integration check, and the fixtures cover the REST-envelope vs MCP structuredContent boundary. Co-Authored-By: Claude Opus 5.5 --- docs/plans/2026-10-23-opportunities-panel.md | 105 ++++++++++++++----- 1 file changed, 78 insertions(+), 27 deletions(-) diff --git a/docs/plans/2026-10-23-opportunities-panel.md b/docs/plans/2026-10-23-opportunities-panel.md index cac6a13..4fb4349 100644 --- a/docs/plans/2026-10-23-opportunities-panel.md +++ b/docs/plans/2026-10-23-opportunities-panel.md @@ -1,10 +1,11 @@ # Opportunities panel (the first MCP App panel) **Updated · 2026-10-23.** Owner: AIPOS (Marty Bradley). Planned; nothing is built here yet. -The engine side is complete: discovery-engine feature 27 (`27_engine_mcp_opportunities_panel`, +The engine binding is complete: discovery-engine feature 27 (`27_engine_mcp_opportunities_panel`, PRs #151–#156). The engine binds this panel to `list_opportunities` and serves it as -`ui://aipos/opportunities` once a release is pinned there. This repository authors, brands, -tests and releases the panel. +`ui://aipos/opportunities` once a release is pinned there. The SDK decision below requires an +engine bundle-budget change before that pin. This repository authors, brands, tests and releases +the panel. ## Purpose and ownership @@ -38,17 +39,19 @@ Keeping it in a separate top-level folder keeps the two release paths from being panels/ BRAND.md # how every panel carries the brand (done in this PR) opportunities/ - template.html app.mjs model.mjs style.css # source, dependency-free like viewers/portfolio - bridge.mjs # the MCP Apps host bridge (see "Host bridge") - build.py # inlines everything into one HTML file; --check for freshness + template.html app.mjs model.mjs style.css # vanilla JavaScript UI; pure model + bridge.mjs # narrow adapter around the official MCP Apps SDK + package.json package-lock.json # exact SDK/build-tool pins and locked dependencies + build.mjs # bundles SDK and source into one HTML file; --check for freshness dist/opportunities.html # committed build output, reviewed as generated code model.test.mjs check-page.mjs fixtures/ tests/test_panel_contract.py # the engine contract, run on dist/ in pytest ``` -`model.mjs` is pure: it shapes tool results into rows and filters, so it can be shared with -`viewers/` later. Nothing under `panels/` is packaged into the plugin, so a panel change needs -no plugin version bump until the "Afterwards" step below. +`model.mjs` is pure and imports no SDK: it shapes normalized tool data into rows and filters, +so it can be shared with `viewers/` later. `bridge.mjs` owns SDK integration and normalizes MCP +results before passing data to the model. Nothing under `panels/` is packaged into the plugin, +so a panel change needs no plugin version bump until the "Afterwards" step below. ## Behaviour @@ -93,13 +96,52 @@ The panel speaks JSON-RPC over `postMessage` with the host: - `ui/notifications/host-context-changed` follows theme changes; - `ui/open-link` opens record links. -Re-check these names against the spec version the target host implements before building. - -**Decision for increment 2:** a hand-written `bridge.mjs` covering only the messages above, or the -official SDK `@modelcontextprotocol/ext-apps` (2.0.3 at writing) bundled in. Hand-written keeps the -file small and dependency-free and makes the tool-name scan trivial to reason about. The SDK -tracks the spec. Recommendation: hand-written, unless the target host needs negotiation the SDK -handles. Either way, the contract test below decides. +These are protocol responsibilities delegated to the SDK, not messages to implement by hand. +Verify the pinned SDK against the spec version the target host implements in increment 2. + +**Decision for increment 2 — SDK selected by Marty Bradley, 2026-10-23.** Use the official +`@modelcontextprotocol/ext-apps` SDK behind a small `bridge.mjs` adapter. Start with exact version +`2.0.3`, the version measured during review; pin the build tool and commit the dependency lock. +Use the app-side SDK with a vanilla JavaScript UI. The SDK and all dependencies are bundled +locally into the HTML; no CDN or runtime package loading is introduced. + +The adapter owns initialization, host context, tool-result normalization and the three allowed +read calls, plus opening record links and the optional conversation hand-back. Register result +handlers before connecting; handle transport failures and tool-result errors separately. Keep +the SDK instance private to the adapter and expose named application operations, rather than an +unrestricted tool-call method. The SDK handles protocol transport and lifecycle; application +state, loading, errors and selection remain panel responsibilities. + +Increment 2 proves a minimal panel in Claude Desktop before the full page is built: receive the +initial result, make a detail call through the host, follow theme changes and resize. Test the +hand-back and its rejection path if Q4 puts it in v1. Record the host and SDK versions. A mock +host supports repeatable tests, but does not replace this early integration check or the final +release check in increment 6. + +### Required engine bundle-budget change + +The existing **256,000-byte** ceiling does not fit the straightforward SDK builds measured in +the 2026-10-23 review. A minimal app exercising the planned bridge methods, bundled with esbuild +`0.28.2`, minification and tree shaking, produced: + +| SDK 2.0.3 entry point | JavaScript bytes, before panel markup and styles | +|---|---:| +| `@modelcontextprotocol/ext-apps` | 601,224 | +| `@modelcontextprotocol/ext-apps/app-with-deps` | 455,884 | + +These are probe results, not the final release size or a host performance measurement. Both +builds passed the forbidden engine-tool identifier scan. Increment 2 records the selected entry +point, dependency lock, reproducible build command and complete HTML byte count. + +**Proposed new ceiling: 1 MiB (1,048,576 bytes) for the complete, uncompressed HTML file.** This +is a pending cross-repository contract change, not an existing engine allowance. Before the SDK +panel can be pinned or the early engine/host check can pass, discovery-engine must update its +loader limit, boundary tests, API handoff and feature 27 budget documentation. Update +`panels/BRAND.md`'s size constraint and this repository's contract test to the same agreed limit. +Keep the single-file, UTF-8, hash verification, no-external-requests and allowed-tool rules. + +Do not fall back to a hand-written bridge merely to pass the old limit. If the budget change +cannot proceed, revisit that constraint explicitly; the selected implementation remains the SDK. ## Brand @@ -116,7 +158,9 @@ Every panel PR carries its checklist and light and dark screenshots, reviewed by `tests/test_panel_contract.py`, in pytest, so the `validate.yml` pytest job runs it on every PR. This is the primary control; the engine's load-time scan is only a tripwire. -- `dist/opportunities.html` is **at most 256,000 bytes** and valid UTF-8. +- `dist/opportunities.html` is valid UTF-8 and within the **agreed engine byte limit**. The + current limit is 256,000 bytes; the SDK plan proposes 1,048,576 bytes as described above. + The panel test must match the engine's revised limit before release, including boundary cases. - **No external requests:** no `http(s)://` in `src`, `href` (except record links opened through `ui/open-link`), `@import`, `url(…)` or `fetch(`; no ``; no web fonts. - **Tool names:** names `list_opportunities`, `get_problem` and `list_evidence`, and **none** of @@ -124,20 +168,25 @@ This is the primary control; the engine's load-time scan is only a tripwire. file, comments and bundled code included. The forbidden list mirrors discovery-engine's `src/engine/api/mcp/panels.py` `ENGINE_TOOL_NAMES`; the test cites that file so the two are updated together. -- `build.py --check` passes: the committed `dist/` is what the source builds. This and the - model's `node --test` need their own `validate.yml` steps, added in increment 4; the existing - Node steps cover only `viewers/`. +- After `npm ci` in `panels/opportunities/`, `node build.mjs --check` passes: the committed + `dist/` is what the locked source and dependencies build. This and the model's `node --test` + need their own `validate.yml` steps, added in increment 4; the existing Node steps cover only + `viewers/`. ## Increments and acceptance | Increment | Result | Completion evidence | |---|---|---| | 1 — Brand and plan | `panels/BRAND.md`; this plan | Reviewed and merged | -| 2 — Model and fixtures | `model.mjs` (rows, filters, sort, paging state, provenance), fixtures from discovery-engine's published samples (`opportunities.json`, `problem-detail.json`, `problem-evidence.json`), the bridge decision | `node --test panels/opportunities/model.test.mjs`; fixtures cite their source paths and SHA-256 | -| 3 — Page, bridge, build | `template.html`, `app.mjs`, `style.css`, `bridge.mjs`, `build.py` producing `dist/opportunities.html`; a mock-host page for local checks | `check-page.mjs` drives the mock host with Playwright (reusing the journey viewer's pinned install): open, filter, drill in, error and empty states, a partial host-context update, keyboard only; hand back **only if Q4 put it in v1**, asserting the exact `ui/message` payload; light and dark screenshots | -| 4 — Contract test and CI | `tests/test_panel_contract.py`; **new `validate.yml` steps** for `python panels/opportunities/build.py --check` and `node --test panels/opportunities/model.test.mjs`. Today's Node steps test only the viewers | Each check proven able to fail (a planted forbidden name, an external URL, an oversized file, a stale `dist/`); the increment is not done until the new steps run on a PR | +| 2 — SDK integration, model and fixtures | Pinned SDK/build dependencies, minimal `bridge.mjs` and `build.mjs`, early Claude Desktop check after the engine budget change; `model.mjs` (rows, filters, sort, paging state, provenance); domain fixtures from the engine's published samples plus actual MCP result/error fixtures | Reproducible full HTML byte count; initial result, detail call, theme and resize work in the real host; hand-back if Q4 includes it; `node --test panels/opportunities/model.test.mjs`; fixtures cite source paths and SHA-256 | +| 3 — Page and interactions | `template.html`, `app.mjs`, `style.css`, completed `bridge.mjs` and `build.mjs` producing `dist/opportunities.html`; a mock-host page for local checks | `check-page.mjs` drives the mock host with Playwright (reusing the journey viewer's pinned install): open, filter, drill in, error and empty states, a partial host-context update, keyboard only; hand back **only if Q4 put it in v1**, asserting the exact `ui/message` payload; light and dark screenshots | +| 4 — Contract test and CI | `tests/test_panel_contract.py`; **new `validate.yml` steps** for panel `npm ci`, `node panels/opportunities/build.mjs --check` and `node --test panels/opportunities/model.test.mjs`. Today's Node steps test only the viewers | Each check proven able to fail (a planted forbidden name, an external URL, an oversized file, a stale `dist/`); the increment is not done until the new steps run on a PR | | 5 — Release | Tag `panels-opportunities-v`; `gh release` with `opportunities.html` and its SHA-256 in the notes | Release published; SHA-256 recomputed from the downloaded asset | -| 6 — Pin and verify in a host | discovery-engine bump PR (its `DEPLOYMENT.md` part F1); the engine run with `ENGINE_MCP_PANELS_ENABLED=true` (stdio for Claude Desktop, or the hosted engine via F2) | `/health` shows `bound` at the release version; the panel renders in Claude Desktop in light and dark; drill-in and hand-back work against the real engine | +| 6 — Pin and verify in a host | discovery-engine bump PR (its `DEPLOYMENT.md` part F1); the engine run with `ENGINE_MCP_PANELS_ENABLED=true` (stdio for Claude Desktop, or the hosted engine via F2) | `/health` shows `bound` at the release version; the panel renders in Claude Desktop in light and dark; drill-in works against the real engine, and hand-back works if Q4 includes it | + +The published domain samples use REST envelopes (`{data, error}`). MCP `structuredContent` +corresponds to their `data` field. Include captured MCP results and errors in the bridge/mock-host +fixtures so the tests verify this boundary rather than passing REST envelopes directly to the UI. **Afterwards:** update the "Native in-chat panel (MCP App): Not available" row in `plugins/aipos/references/panel-contract.md`, which is a plugin change and needs a version bump. @@ -145,8 +194,9 @@ Tell the skills that call `list_opportunities` that a panel may be showing. ## For the demo -Increments 2–6 must land, in order. The engine side needs nothing more than the pin (6) and the -switch. For a local demo in Claude Desktop, the engine's server entry in +Increments 2–6 must land, in order. The engine budget change is a prerequisite for increment 2's +real-host check; the final demo also needs the release pin (6) and the switch. For a local demo +in Claude Desktop, the engine's server entry in `~/Library/Application Support/Claude/claude_desktop_config.json` needs these in its `env`: - `ENGINE_MCP_PANELS_ENABLED=true`; - `ENGINE_MCP_TOKEN`; @@ -155,7 +205,8 @@ switch. For a local demo in Claude Desktop, the engine's server entry in ## Open questions -1. The bridge implementation: hand-written, or the SDK (decide in increment 2). +1. Land the engine budget change for the selected SDK: proposed ceiling 1 MiB; confirm the + complete bundle size and host loading in increment 2. The bridge implementation is decided. 2. The official SVG artwork for the mark (`BRAND.md` "Open items"). The panel ships without a mark until it arrives. 3. Brand book discrepancies for the brand owner (`BRAND.md` "Source notes"): the secondary blue