working-on-views — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited working-on-views (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Read docs/views.md for architecture, response format, and constraints. Read docs/view-design.md for when to build a view, interaction patterns, and visual principles.
worker/src/mcp/views.gen.ts is a committed build artifact — CI does not rebuild it. After modifying ANY .svelte file (in views/, worker/src/mcp/views/, or plugins/*/reference/views/), you MUST:
just build-viewsviews.gen.ts alongside your other changesForgetting this means production serves stale view HTML even after a successful cloud deploy. The deploy will succeed, the tests will pass, but users see the old view.
just build-views # Rebuild views.gen.ts (required after any .svelte change)
just test-worker # Handler + tool tests
just storybook-views # Visual verification on port 6007Game state views — one self-contained HTML per tool. Component + bridge bundled together.
worker/src/mcp/views/<slug>.sveltesearch-saves.svelte → tool search_savesReference views — one bundled HTML for query_reference containing ALL reference module components. Routes on structuredContent.module.
plugins/<game>/reference/views/<module-id>.sveltecard-search.svelte → module card_search.svelte file and rebuilding is sufficient.<name>.svelte — Svelte 5 component: let { data } = $props()<name>.stories.svelte — Storybook story with fixture datatools.ts to return viewResult(structuredContent, narrative) instead of textResult()just build-views then just test-workerviews.gen.ts with other changes — CI does not rebuild it// Old (presentation hints — being replaced):
textResult(data, "Display as card gallery...");
// New (structured content for view rendering):
viewResult({ cards, total: cards.length }, "Found 5 cards.");
// For query_reference, handler injects module automatically:
// → { module: "card_search", cards: [...], total: 5 }viewResult() returns { structuredContent, content }. The view renders structuredContent as UI. content carries BOTH the narrative AND the same data as JSON text — this is critical because Claude hides structuredContent from the model when a widget renders. Without data in content, the model is blind to what the view shows and cannot reason about it.
The bridge at views/src/bridge.ts uses @modelcontextprotocol/ext-apps's App class. It handles the ui/initialize handshake that MCP hosts require before sending data.
Never use raw `window.addEventListener("message", ...)` — the host will not send tool results without the initialization handshake. The App class handles this.
Views render the AI's synthesis. The conversation drives interaction. See docs/view-design.md.
Views are passive renderers of data the LLM assembled. If the player wants to go deeper, they type another message. The view presents; the LLM decides what to show next.
"pip" and "inline".structuredContent.Savecraft design tokens in views/src/view.css: --color-bg, --color-panel-bg, --color-border, --color-gold, --color-text, --color-text-dim, --color-text-muted, --font-pixel, --font-heading, --font-body.
Host theme integration (optional, for native look):
import { applyDocumentTheme, applyHostStyleVariables, applyHostFonts } from "@modelcontextprotocol/ext-apps";
app.onhostcontextchanged = (ctx) => {
if (ctx.theme) applyDocumentTheme(ctx.theme);
if (ctx.styles?.variables) applyHostStyleVariables(ctx.styles.variables);
if (ctx.styles?.css?.fonts) applyHostFonts(ctx.styles.css.fonts);
};Host provides 50+ CSS variables: --color-background-*, --color-text-*, --font-sans, --font-mono, --border-radius-*, --shadow-*.
Default iframe CSP blocks external resources. To use external fonts, images, or APIs:
// In handler.ts, add to resource or tool _meta.ui:
_meta: {
ui: {
resourceUri: "ui://savecraft/reference.html",
csp: {
resourceDomains: ["https://fonts.googleapis.com", "https://fonts.gstatic.com"], // fonts, images, styles
connectDomains: ["https://api.example.com"], // fetch/XHR/WebSocket
}
}
}Key CSP facts:
script-src 'self' 'unsafe-inline' — inline scripts OK, blob: URLs blockedstyle-src 'self' 'unsafe-inline' — inline styles OK, external stylesheets blocked unless declared in resourceDomainsconnect-src 'none' — no network unless declared in connectDomainsfont-src 'self' — no external fonts unless declared in resourceDomainsjust build-views runs views/scripts/build.ts:
.svelte files (excluding .stories.svelte) from worker/src/mcp/views/ and plugins/*/reference/views/worker/src/mcp/views.gen.ts — committed, not rebuilt by CIThe handler imports VIEWS from views.gen.ts and auto-wires resources/list, resources/read, and _meta.ui.resourceUri on tools/list.
Every view includes a collapsed legal footer rendered by views/src/Attribution.svelte. This is automatic — view authors don't need to do anything.
How it works: The build pipeline reads [attribution].sources from each plugin's plugin.toml, resolves source keys against shared presets in views/src/attributions.ts, and embeds the result as window.__ATTRIBUTION__ in the compiled HTML. The Attribution component reads this global and renders a collapsed ▸ Legal · Source1 · Source2 footer that expands to show full disclaimers on click.
[attribution] or uses an unknown source keyTo add a new attribution source: add it to SOURCES in views/src/attributions.ts, then reference it from plugin.toml.
Every reference view should display its game's icon as a subtle centered watermark. This is a first-class part of the Savecraft visual language — it gives each game identity without competing with content.
How it works:
watermark?: string prop — renders a centered, semi-transparent (10% opacity) <img> that shows through gaps between content elementsicon_url into structuredContent via resolveIconUrl() (uses per-isolate manifest cache)data.icon_url to their outer <Panel watermark={data.icon_url}>icon_url in fixture data: const iconUrl = "/plugins/<game>/icon.png"Rules:
watermark={data.icon_url}icon_url?: stringicon_url: iconUrl in fixture data so the watermark is visible during developmentresolveIconUrl(plugins, serverUrl, gameId) is the single source of truth for icon URL construction — never duplicate the manifest lookupFor MtgCard specifically: The iconUrl prop passes through to <Panel watermark={iconUrl}> internally. Card-search passes data.icon_url to each MtgCard's iconUrl prop.
Views use a two-level hierarchy:
<Panel nested> with a <span class="sub-label"> heading (defined in view.css as a global utility class).Anti-patterns:
count prop — the number badge in the upper-right is confusing and visually noisy. If a count matters, show it in the content area.<script lang="ts">
// Props from structuredContent
let { data }: { data: { cards: Card[]; total: number } } = $props();
</script>
<div class="gallery">
{#each data.cards as card}
<div class="card">{card.name}</div>
{/each}
</div>
<style>
.gallery { display: grid; gap: 10px; padding: 16px; }
.card {
background: var(--color-panel-bg);
border: 1px solid var(--color-border);
border-radius: 8px;
padding: 12px;
font-family: var(--font-heading);
color: var(--color-text);
}
</style><script module>
import { defineMeta } from "@storybook/addon-svelte-csf";
import CardSearch from "./card-search.svelte";
const { Story } = defineMeta({ title: "Reference/CardSearch", tags: ["autodocs"] });
</script>
<Story name="MultipleCards">
<CardSearch data={{ cards: [{ name: "Lightning Bolt", ... }], total: 1 }} />
</Story>just build-views. Forgetting to rebuild and commit after .svelte changes means staging/production serves stale views.extensions: { "io.modelcontextprotocol/ui": {} } in initialize response. Without this, hosts don't render views.2025-06-18.views/src/bridge.ts # App class bridge (do not reimplement)
views/src/view.css # Design system tokens
views/src/attributions.ts # Attribution presets registry
views/src/Attribution.svelte # Collapsed legal footer component
views/scripts/build.ts # Build → views.gen.ts
views/.storybook/ # Storybook config (port 6007)
worker/src/mcp/views.gen.ts # GENERATED — VIEWS record (commit this)
worker/src/mcp/handler.ts # resources/list, resources/read, _meta.ui
worker/src/mcp/tools.ts # viewResult(), textResult()
worker/src/mcp/views/ # Game state views + stories
plugins/*/reference/views/ # Reference views + stories~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.