Repository navigation
2026 10 04 show
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: One MCP App tool, show(uri), draws six resources in the existing app shell — context, files, a folder (screenshots, downloads), the flow list as a card scroller, one flow — and a flow card opens that flow in place. session_files goes.
Architecture: mcp/show.py maps a URI to a view name, reads the resource through the server, and returns {component, uri, data}. The tool carries the shared app declaration with visibility: ["model", "app"], so the shell can call it for drill-down. ui/src/App.svelte dispatches on component, keeps a small navigation stack, applies the host's theme, sizes itself, and shares the shown URI with updateModelContext. New Svelte views reuse FileGrid/Lightbox and browserMark.
Tech Stack: Python 3.10+, FastMCP 4 (fastmcp.apps.AppConfig), pytest; Svelte 5, Vite, vitest + @testing-library/svelte, @modelcontextprotocol/ext-apps 2.0.0 (App, applyDocumentTheme, applyHostStyleVariables, applyHostFonts).
Spec: docs/superpowers/specs/2026-10-04-show-design.md — read Rulings, Non-goals and The design first.
- The six showable URIs and their components are exactly:
session://current→context,session://files→files,session://files/screenshots→folder,session://files/downloads→folder,flow://flows→flows,flow://flows/{name}→flow. One table inmcp/show.pyis the only place this mapping lives. -
showreturns the resource's own JSON asdata; it computes nothing new for a view. -
showcarriesvisibility: ["model", "app"]and is listed only for a client that renders apps (the existing app-tool hiding).session_filesis removed outright (one user, no back-compat). - No Run button, no editing, no
sendMessage, nopip. - Inline views never scroll vertically inside; carousels show equal cards with the next one peeking; colours come from the host's style variables with the existing
prefers-color-schemefallback; skeletons, not spinners; no dropdowns. - The app bundle stays within its gzip budget:
npm --prefix ui run size(110 KB forapp). - Python tests:
PYTHONPATH=$PWD:$PY python3 -S -m pytest -q -p no:randomly <paths>withPY=/tmp/claude-1000/-projects-cluster/c609cf8e-e357-4d7e-8988-ef046fbf64f9/scratchpad/pylibs(-Sis required in this pod). Known env-only failures: tests/test_boundaries.py ×4, tests/test_shutdown.py sigterm, tests/test_wiki.py::test_the_generated_pages_are_current (usepython3 -S scripts/generate_wiki.py --check). Lint:PYTHONPATH=$PY python3 -S -m ruff check .(CI runs onlyruff check; neverruff formatfiles you did not create). - UI:
npm --prefix ui cionce in the worktree, thennpm --prefix ui test,npm --prefix ui run check,npm --prefix ui run lint,npm --prefix ui run build,npm --prefix ui run size. - Real hosts never appear in tests or examples (
example.com, userdrk). Copy is terse (memory: UI copy is terse — dots, not "set"; no helper sentences). - Commits: one per task, house style, ending with
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>.
| File | Responsibility |
|---|---|
Create kubed/selenium_flow/mcp/show.py
|
The view table, view_for(uri), and the show tool. |
Modify kubed/selenium_flow/mcp/apps.py
|
config_for gains visibility=["model", "app"]. |
Modify kubed/selenium_flow/server.py
|
Register show; add it to the app tools. |
Modify kubed/selenium_flow/http/files.py
|
Remove the session_files tool and FILES_TOOL; register returns set(). |
Create tests/test_show.py
|
The tool through the MCP surface. |
Modify tests/test_files_and_admin.py, tests/test_kept_files.py
|
Drop / move the session_files tests. |
Create ui/src/lib/views/ContextView.svelte, FolderView.svelte, FilesView.svelte, FlowsView.svelte, FlowView.svelte (+ *.test.ts) |
One view per component name. Each takes data and an optional onshow(uri). |
Modify ui/src/App.svelte, ui/src/App.test.ts, ui/src/app.css
|
Dispatch, navigation stack, host theme, sizing, fullscreen, model context. |
| Docs |
skills/selenium-flow/SKILL.md, references/FLOWS.md, references/SESSIONS.md, README.md, AGENTS.md, CHANGELOG.md, scripts/generate_wiki.py prose if it names session_files, wiki/ regenerated. |
Files:
- Create:
kubed/selenium_flow/mcp/show.py - Modify:
kubed/selenium_flow/mcp/apps.py(config_for),kubed/selenium_flow/server.py(~line 165–210, besidefiles.register/apps.register),kubed/selenium_flow/http/files.py(FILES_TOOLat line 94, the tool at ~732–747,return {FILES_TOOL}at 769) - Create:
tests/test_show.py - Modify:
tests/test_files_and_admin.py(~466–477),tests/test_kept_files.py(~447–456),tests/golden/tools-on.json/tools-off.jsonif they listsession_files(regenerate withGOLDEN_UPDATE=1)
Interfaces:
-
Produces:
show.VIEWS: tuple[tuple[re.Pattern, str], ...],show.view_for(uri: str) -> str(raisesValueErrornaming the showable URIs),show.SHOWABLE: tuple[str, ...](the six as written),show.TOOL = "show",show.register(mcp, app_config) -> set[str](returns{"show"}). Tool result: structured content{"component": str, "uri": str, "data": dict}. -
Step 1: Write the failing tests (
tests/test_show.py). Use the existing fixtures that give a server with a flow store and kept files (readtests/conftest.pyandtests/test_kept_files.pyforkept_server/ flow-store fixtures; reuse them rather than inventing new ones). Call the tool through FastMCP'sClient(server.mcp)withclient_infonaming a client andpatch.object(apps, "supported", return_value=True)where listing matters, and readresult.structured_content:
"""show(uri): one MCP App tool that draws a resource by its URI."""
import pytest
from fastmcp import Client
from fastmcp.exceptions import ToolError
from unittest.mock import patch
from kubed.selenium_flow.mcp import apps, show
pytestmark = pytest.mark.unit
@pytest.mark.parametrize(
("uri", "component"),
[
("session://current", "context"),
("session://files", "files"),
("session://files/screenshots", "folder"),
("session://files/downloads", "folder"),
("flow://flows", "flows"),
("flow://flows/login", "flow"),
],
)
def test_every_showable_uri_has_one_view(uri, component):
assert show.view_for(uri) == component
@pytest.mark.parametrize(
"uri", ["skill://selenium-flow/SKILL.md", "flow://schema", "session://files/a.png",
"flow://flows/", "session://current/x", "secret://secrets"],
)
def test_anything_else_is_refused_naming_what_can_be_shown(uri):
with pytest.raises(ValueError) as refused:
show.view_for(uri)
for showable in show.SHOWABLE:
assert showable in str(refused.value)
async def test_show_returns_the_resources_own_json(<flow-store server fixture>):
# save a flow named "login" in the caller's library first (see how
# tests/test_flows*.py do it), then:
async with Client(server.mcp) as c:
shown = (await c.call_tool("show", {"uri": "flow://flows/login"})).structured_content
read = json.loads((await c.read_resource("flow://flows/login"))[0].text)
assert shown == {"component": "flow", "uri": "flow://flows/login", "data": read}
async def test_a_missing_flow_is_refused(<flow-store server fixture>):
async with Client(server.mcp) as c:
with pytest.raises(ToolError):
await c.call_tool("show", {"uri": "flow://flows/nope"})
async def test_show_is_listed_only_for_a_client_that_renders_apps(built_ui, server):
names = {t.name for t in await server.mcp.list_tools()}
assert "show" not in names
with patch.object(apps, "supported", return_value=True):
tools = {t.name: t for t in await server.mcp.list_tools()}
assert "show" in tools
assert "session_files" not in tools
async def test_the_app_may_call_show_itself(built_ui, server):
tool = await server.mcp.get_tool("show")
ui = (tool.meta or {}).get("ui") or {}
assert ui.get("visibility") == ["model", "app"]
assert ui.get("resourceUri") == apps.RESOURCE_URI(Adjust the meta lookup to how FastMCP 4.0.x exposes AppConfig on a tool — read fastmcp/apps/config.py and an existing assertion on session_files's app config if one exists; the requirement is that the published tool says visibility: ["model", "app"] and points at the shell. Include the session-scoped cases session://current, session://files, and one folder through the client too, with the named-session caller the files tests use.)
-
Step 2: Run them to see them fail —
ModuleNotFoundError: kubed.selenium_flow.mcp.show. -
Step 3: Implement
kubed/selenium_flow/mcp/show.py:
"""show(uri): draw one resource as an MCP App, keyed by its URI.
One tool and one shell for every view: the URI says which resource, the table
below says which component draws it, and the data is the resource's own JSON —
so a view cannot drift from what the resource serves. It is not read_resource:
that is the model's reading tool, and an app on it would draw a UI on every read
the model makes to think (spec 2026-10-04, ruling 1).
"""
from __future__ import annotations
import json
import re
from fastmcp.server.dependencies import get_context
from fastmcp.tools.tool import ToolResult
from ..core.annotations import reads
TOOL = "show"
# First match wins. The only place a URI is tied to a view.
VIEWS: tuple[tuple[re.Pattern[str], str], ...] = (
(re.compile(r"session://current"), "context"),
(re.compile(r"session://files"), "files"),
(re.compile(r"session://files/(screenshots|downloads)"), "folder"),
(re.compile(r"flow://flows"), "flows"),
(re.compile(r"flow://flows/[^/]+"), "flow"),
)
SHOWABLE = (
"session://current", "session://files", "session://files/screenshots",
"session://files/downloads", "flow://flows", "flow://flows/{name}",
)
def view_for(uri: str) -> str:
for pattern, component in VIEWS:
if pattern.fullmatch(uri):
return component
raise ValueError(f"{uri} has no view; show draws {', '.join(SHOWABLE)}")
def register(mcp, app_config) -> set[str]:
@mcp.tool(
name=TOOL,
description=(
"Draw a resource for the person to see: "
+ ", ".join(SHOWABLE)
+ ". For you to read one, read the resource instead. The view the "
"person is looking at is shared with you as context."
),
app=app_config,
annotations=reads("Show a resource"),
)
async def show(uri: str) -> ToolResult:
component = view_for(uri)
result = await get_context().fastmcp.read_resource(uri)
data = json.loads(result.contents[0].content)
payload = {"component": component, "uri": uri, "data": data}
return ToolResult(structured_content=payload)
return {TOOL}Check ToolResult's constructor and whether a structured result also needs a text content block for hosts that ignore structured content (mirror what session_files's dict return produced: FastMCP turned the dict into both). If returning the plain dict gives the same published result, return the dict — simpler. Verify read_resource's return shape against mcp/mirror.py, which already calls it, and reuse its not-found handling so a missing flow is a ValueError (the house refusal), not a stack trace.
apps.config_for: add visibility=["model", "app"] to the AppConfig(...); update its docstring in one clause (the app calls show for drill-down).
server.py: after files.register(...), app_tools |= show.register(self.mcp, app_config) (import show from .mcp). http/files.py: delete the session_files tool and FILES_TOOL; register returns set(); keep sections() — http/admin/files.py still uses it; fix the module docstring paragraph that names session_files.
Tests: remove the two session_files listing tests in test_files_and_admin.py (their property now lives in test_show.py); move test_the_file_listing_never_opens_a_browser (test_kept_files.py) to exercise show("session://files") and show("session://files/downloads") instead of FILES_TOOL — the "never opens a browser" property must survive.
-
Step 4: Run the new and touched tests, regenerate goldens (
GOLDEN_UPDATE=1 … tests/test_golden.py; onlyshow/session_filesmay differ), the whole suite once, ruff. -
Step 5: Commit —
show(uri) draws a resource as an MCP App, and session_files goes.
Files:
- Create:
ui/src/lib/views/ContextView.svelte,FolderView.svelte,FilesView.svelte,FlowsView.svelte,FlowView.svelte, and a*.test.tsbeside each - Modify:
ui/src/lib/types.ts(types for the data each view takes)
Interfaces:
-
Consumes: the
datashapes the resources serve — read them in the server code, not from memory:session://current(session/sessions.py::describe),session://files(http/files.py, the root listing:{session, count, files, folders: [{name, uri, count, browser?}]}), a folder (http/files.py::folder:{session, folder, uri, count, files, browser?}),flow://flows(flows/api.py::catalogue:{session, count, flows: [{name, description, parameters?, step_count, shared}]}— confirm the summary fields inflows/store.py::summaries),flow://flows/{name}(flows/api.py::read_one). -
Produces: every view is
let { data, onshow }: { data: <Type>; onshow?: (uri: string) => void } = $props(). A view never calls the host itself; drill-down goes throughonshow, and a view with noonshowrenders its items as non-clickable. -
Step 1: Tests first, one file per view, with testing-library (
render(View, { props: { data, onshow } })), each asserting:-
ContextView: session name, browser mark,live/idlepill, the URL as a link (safeHref), window, the principal's username (or kind), site count;principal: nullshows nothing for it; a session with no URL shows the muted "nowhere yet". -
FolderView: oneFileGridwith the folder's files, a title fromdata.folderand a count pill; empty folder shows the empty line; clicking an image tile opens theLightbox(asFileGridalready does — assert the lightbox element appears). -
FilesView: the kept files in oneFileGrid, plus a chip perdata.foldersentry showing its name and count; clicking a chip callsonshow(folder.uri); withoutonshowthe chips are not buttons. -
FlowsView: one card per flow with name, description,N params · M steps, a shared mark whenshared; the cards sit in one horizontal scroller (role="list"on the scroller,role="listitem"per card); clicking a card callsonshow("flow://flows/" + encodeURIComponent(name)); no flows → one empty line, no scroller; withoutonshowcards are not buttons. -
FlowView: name, description, each parameter (name, type, default, required), the first 6 steps asn. tool — summarywithonError: continuemarked, and a+N moreline when there are more;expandedprop true shows every step.
-
-
Step 2: Run
npm --prefix ui test— they fail (components missing). -
Step 3: Implement the five components in the style of
lib/SessionSummary.svelteandlib/FileSections.svelte(same class names and tokens; readapp.cssfirst). Rules: no vertical scrolling inside a view; the flow scroller isoverflow-x: auto; scroll-snap-type: x mandatory, cardsflex: 0 0 min(240px, 80%)so the next one peeks,scroll-padding-inline: var(--safe-left, 0); description clamped to two lines; tap targets ≥ 44px; every clickable card/chip is a<button>with an accessible name.FlowView's step summary is the tool name plus the first argument value that is a string (selector, url or text), truncated to 60 characters — one helper informat.tswith its own unit test. -
Step 4: Run
npm --prefix ui test,run check,run lint— all clean. -
Step 5: Commit —
App views for show: context, files, a folder, the flow scroller, one flow.
Files:
- Modify:
ui/src/App.svelte,ui/src/App.test.ts,ui/src/app.css
Interfaces:
-
Consumes: the five views (Task 2); the tool result
{component, uri, data}(Task 1). -
Produces: nothing for later tasks.
-
Step 1: Tests first (
App.test.ts; extend the existingvi.mock('@modelcontextprotocol/ext-apps')class withcallServerTool,updateModelContext,requestDisplayMode,getHostContext,getHostCapabilitiesasvi.fns, and exportapplyDocumentTheme/applyHostStyleVariables/applyHostFontsstubs):- each
component(context,files,folder,flows,flow) renders its view from atoolresultwhosestructuredContentis{component, uri, data}; - an unknown component still says
Nothing to show for "<name>".; a host that fails to connect still says why; - a flow card click calls
callServerTool({ name: 'show', arguments: { uri: 'flow://flows/login' } }), the returnedstructuredContentis drawn, and a Back button appears; Back returns to the list and disappears; -
updateModelContextis called after the first result and after each push/pop with content text startingShowingand the URI; -
getHostCapabilities()withoutserverTools→ the flow cards render withoutonshow(not buttons); - the fullscreen button shows on
flowonly whengetHostContext().availableDisplayModesincludesfullscreen, and clicking it callsrequestDisplayMode({ mode: 'fullscreen' })and passesexpandedtoFlowView; -
fileSectionsis gone from the table.
- each
-
Step 2: Run — they fail.
-
Step 3: Implement in
App.svelte:-
const VIEWS = { context: ContextView, files: FilesView, folder: FolderView, flows: FlowsView, flow: FlowView }. -
let stack = $state.raw<Shown[]>([])whereShown = { component, uri, data }; the firsttoolresultreplaces the stack with[result]. -
canShow = !!host.getHostCapabilities()?.serverTools;onshow = canShow ? (uri) => push(uri) : undefined;pushcallshost.callServerTool({ name: 'show', arguments: { uri } }), and on success appends itsstructuredContent; onisErroror a throw, shows the error line above the current view without losing it. - Back pops one level. After any stack change:
host.updateModelContext({ content: [{ type: 'text', text: \Showing ${top.uri}` }] }).catch(() => {})`. - Theme: after
connect(), applygetHostContext()'stheme/styles.variables/styles.css.fontswith the SDK helpers, and again inhost.onhostcontextchanged. Keepapp.css'sprefers-color-schemerules as the fallback, mapped onto the host's variable names (--color-background-primary,--color-text-primary, … with today's colours as fallbacks). - Size:
new Host(info, {}, { autoResize: true })(check the constructor signature), and after each render ($effect+tick()), setdocument.documentElement.style.height = document.body.scrollHeight + 'px'. Remove any100vh. - Fullscreen: a small button on views that support it (
flow), shown whenavailableDisplayModesincludesfullscreen;requestDisplayMode({ mode })toggles, and the mode the host returns is the one used.
-
-
Step 4: Run the UI gate:
test,check,lint,build,size(within 110 KB), then the Python suite once (built_uitests use the built shell). -
Step 5: Commit —
The app shell: one stack of views, Back, the host's theme, and its own height.
-
skills/selenium-flow/SKILL.md: ashowrow in the tool table (draw a resource for the person: context, files, a folder, flows, one flow·uri); one line inreferences/FLOWS.md("show(flow://flows/<name>)draws one flow;show(flow://flows)the scroller") and inreferences/SESSIONS.md(show(session://current)). -
README.md: one feature line under MCP Apps (advertise). -
AGENTS.md: the apps section — one tool, the view table lives inmcp/show.py, drill-down is the shell callingshowthroughcallServerTool(visibility: ["model", "app"]), notsendMessage(Claude drafts it into the input box), not an app onread_resource;updateModelContextis best effort; the claude.ai height workaround. -
CHANGELOG.md## [Unreleased]: "show(uri)draws a resource as an MCP App — your session, files, a folder, the saved flows as cards, one flow;session_filesis gone." -
scripts/generate_wiki.pyandwiki/Installing.mdprose:session_files→show; regenerate the wiki,--check, commit insidewiki/, commit the pointer. - Commit —
Docs: show draws a resource, and session_files is gone.
- Full gate (Python suite, ruff, UI gate, wiki check). Push the wiki to
masterfirst, then the branch; open the PR (Dr K approved this PR, 2026-10-04) with the spec/plan, the view table, the rulings and the CHANGELOG line. - Watch CI; answer every Copilot / code-scanning thread with
gh(fix or decline with evidence) and resolve it in the same breath. - After merge (Dr K's call): wait for the
:mainimage,kubectl rollout restart -n flow deploy/selenium-flow(the tag ismain, pullAlways; Grid nodes untouched). - Live on Claude web (Dr K): "show me my screenshots", "show me my flows" → click one → Back, "show me the session", light and dark; note anything the host does differently in the spec's status line.
The action pages are generated from openapi.yaml, which is itself generated from the live MCP tool schemas — so they describe the server that shipped, not the one someone remembered. Prose belongs in wiki-notes/<tool>.md in the repo.
selenium-flow · MIT
Start here
Guides
Lifecycle
Going places
Doing things
Getting things out
Site data
Console and network