Skip to content

2026 10 04 show

Kelly Ferrone edited this page Oct 10, 2026 · 1 revision

show Implementation Plan

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.

Global Constraints

  • 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 in mcp/show.py is the only place this mapping lives.
  • show returns the resource's own JSON as data; it computes nothing new for a view.
  • show carries visibility: ["model", "app"] and is listed only for a client that renders apps (the existing app-tool hiding). session_files is removed outright (one user, no back-compat).
  • No Run button, no editing, no sendMessage, no pip.
  • 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-scheme fallback; skeletons, not spinners; no dropdowns.
  • The app bundle stays within its gzip budget: npm --prefix ui run size (110 KB for app).
  • Python tests: PYTHONPATH=$PWD:$PY python3 -S -m pytest -q -p no:randomly <paths> with PY=/tmp/claude-1000/-projects-cluster/c609cf8e-e357-4d7e-8988-ef046fbf64f9/scratchpad/pylibs (-S is 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 (use python3 -S scripts/generate_wiki.py --check). Lint: PYTHONPATH=$PY python3 -S -m ruff check . (CI runs only ruff check; never ruff format files you did not create).
  • UI: npm --prefix ui ci once in the worktree, then npm --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, user drk). 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 structure

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.

Task 1: The show tool, and session_files goes

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, beside files.register / apps.register), kubed/selenium_flow/http/files.py (FILES_TOOL at 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.json if they list session_files (regenerate with GOLDEN_UPDATE=1)

Interfaces:

  • Produces: show.VIEWS: tuple[tuple[re.Pattern, str], ...], show.view_for(uri: str) -> str (raises ValueError naming 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 (read tests/conftest.py and tests/test_kept_files.py for kept_server / flow-store fixtures; reuse them rather than inventing new ones). Call the tool through FastMCP's Client(server.mcp) with client_info naming a client and patch.object(apps, "supported", return_value=True) where listing matters, and read result.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; only show/session_files may differ), the whole suite once, ruff.

  • Step 5: Commit — show(uri) draws a resource as an MCP App, and session_files goes.


Task 2: The views

Files:

  • Create: ui/src/lib/views/ContextView.svelte, FolderView.svelte, FilesView.svelte, FlowsView.svelte, FlowView.svelte, and a *.test.ts beside each
  • Modify: ui/src/lib/types.ts (types for the data each view takes)

Interfaces:

  • Consumes: the data shapes 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 in flows/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 through onshow, and a view with no onshow renders 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/idle pill, the URL as a link (safeHref), window, the principal's username (or kind), site count; principal: null shows nothing for it; a session with no URL shows the muted "nowhere yet".
    • FolderView: one FileGrid with the folder's files, a title from data.folder and a count pill; empty folder shows the empty line; clicking an image tile opens the Lightbox (as FileGrid already does — assert the lightbox element appears).
    • FilesView: the kept files in one FileGrid, plus a chip per data.folders entry showing its name and count; clicking a chip calls onshow(folder.uri); without onshow the chips are not buttons.
    • FlowsView: one card per flow with name, description, N params · M steps, a shared mark when shared; the cards sit in one horizontal scroller (role="list" on the scroller, role="listitem" per card); clicking a card calls onshow("flow://flows/" + encodeURIComponent(name)); no flows → one empty line, no scroller; without onshow cards are not buttons.
    • FlowView: name, description, each parameter (name, type, default, required), the first 6 steps as n. tool — summary with onError: continue marked, and a +N more line when there are more; expanded prop 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.svelte and lib/FileSections.svelte (same class names and tokens; read app.css first). Rules: no vertical scrolling inside a view; the flow scroller is overflow-x: auto; scroll-snap-type: x mandatory, cards flex: 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 in format.ts with 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.


Task 3: The shell

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 existing vi.mock('@modelcontextprotocol/ext-apps') class with callServerTool, updateModelContext, requestDisplayMode, getHostContext, getHostCapabilities as vi.fns, and export applyDocumentTheme / applyHostStyleVariables / applyHostFonts stubs):

    • each component (context, files, folder, flows, flow) renders its view from a toolresult whose structuredContent is {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 returned structuredContent is drawn, and a Back button appears; Back returns to the list and disappears;
    • updateModelContext is called after the first result and after each push/pop with content text starting Showing and the URI;
    • getHostCapabilities() without serverTools → the flow cards render without onshow (not buttons);
    • the fullscreen button shows on flow only when getHostContext().availableDisplayModes includes fullscreen, and clicking it calls requestDisplayMode({ mode: 'fullscreen' }) and passes expanded to FlowView;
    • fileSections is gone from the table.
  • 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[]>([]) where Shown = { component, uri, data }; the first toolresult replaces the stack with [result].
    • canShow = !!host.getHostCapabilities()?.serverTools; onshow = canShow ? (uri) => push(uri) : undefined; push calls host.callServerTool({ name: 'show', arguments: { uri } }), and on success appends its structuredContent; on isError or 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(), apply getHostContext()'s theme / styles.variables / styles.css.fonts with the SDK helpers, and again in host.onhostcontextchanged. Keep app.css's prefers-color-scheme rules 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()), set document.documentElement.style.height = document.body.scrollHeight + 'px'. Remove any 100vh.
    • Fullscreen: a small button on views that support it (flow), shown when availableDisplayModes includes fullscreen; 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_ui tests use the built shell).

  • Step 5: Commit — The app shell: one stack of views, Back, the host's theme, and its own height.


Task 4: Docs

  • skills/selenium-flow/SKILL.md: a show row in the tool table (draw a resource for the person: context, files, a folder, flows, one flow · uri); one line in references/FLOWS.md ("show(flow://flows/<name>) draws one flow; show(flow://flows) the scroller") and in references/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 in mcp/show.py, drill-down is the shell calling show through callServerTool (visibility: ["model", "app"]), not sendMessage (Claude drafts it into the input box), not an app on read_resource; updateModelContext is 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_files is gone."
  • scripts/generate_wiki.py and wiki/Installing.md prose: session_files → show; regenerate the wiki, --check, commit inside wiki/, commit the pointer.
  • Commit — Docs: show draws a resource, and session_files is gone.

Task 5: PR, image, live test

  • Full gate (Python suite, ruff, UI gate, wiki check). Push the wiki to master first, 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 :main image, kubectl rollout restart -n flow deploy/selenium-flow (the tag is main, pull Always; 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.

Clone this wiki locally