Development and verification
Start a development session
Section titled “Start a development session”npx mountly-mcp doctornpx mountly-mcp devdoctor checks Node, the Vite plugin, Playwright for --render, and peer
floors. Fix FAIL lines before you chase handshake bugs.
The CLI reads mountlyMcpViews(), builds the selected View, rebuilds on change,
and opens a two-origin host. Official AppBridge drives the handshake. Mountly
adds the host UI and sandbox proxy that enforce the artifact’s CSP.
Multi-View builds need a name:
npx mountly-mcp dev --app weather_dashboardOptions:
-c, --config <path> Vite config (default: auto-discover)-f, --fixtures <path> named JSON samples (default: mcp.fixtures.json)-s, --server <path> module default-exporting an MCP server or factory-a, --app <name> View to develop; required for a multi-View build-p, --port <number> host port (default: 5179) --no-open do not open a browserWithout --server, each fixture value lands as structuredContent. Each
button hits the real ui/notifications/tool-result path.
Connect a real server
Section titled “Connect a real server”npx mountly-mcp dev --server ./dist/create-server.jsThe module must default-export an unconnected McpServer, a compatible
Mountly server, or a function that returns one. With a server connected,
fixture values become arguments to the model-visible tool linked to the View;
the View receives the handler’s actual result. Tool calls initiated by the View
are routed to that server as well.
Exporting a factory is the safest development shape because each session gets a fresh, unconnected server.
The same connection is available programmatically from mountly-mcp/dev, so a
harness can drive real tools without a transport:
import { connectMcpServer } from "mountly-mcp/dev";
const server = await connectMcpServer("./dist/create-server.js");const toolName = await server.toolFor("ui://weather/dashboard");if (!toolName) throw new Error("no tool is bound to that View");
const result = await server.callTool(toolName, { city: "Bristol" });await server.close();toolFor() returns the model-visible tool bound to that View (the one a host
would call). A module that default-exports something other than a server, or a
factory returning one, is rejected by name rather than failing later inside the
handshake.
Verify artifacts
Section titled “Verify artifacts”npx mountly-mcp verifynpx mountly-mcp verify --rendernpx mountly-mcp verify --render --strictBy default, verification reads dist/mountly-mcp.manifest.json and performs
offline file and metadata checks. Its summary explicitly says static checks only because finding bridge strings does not prove a View can boot. Static
status is not a warning, so verify --strict remains a useful offline gate.
--render launches a real browser, starts the two-origin host, loads every
View, waits for the bridge’s explicit mounted state, and rejects timeouts,
captured mount failures, and empty View roots. Non-empty content can still be
wrong, so keep assertions for important states in component or end-to-end
tests. Install Playwright and its Chromium browser in the CI environment that
runs this tier. --strict fails on any advisory warning.
Accessibility of the assembled View
Section titled “Accessibility of the assembled View”A View that mounts is also audited with axe,
reported as render/a11y warnings: one per violation, with the rule, its
impact, and the first element that fails:
! render/a11y: label (critical impact): Form elements must have labels (1 element, first at input[type="text"])This is the check no component-level tool can make for you. Every component in a View can be individually accessible and the composition still fail, because a missing label, a heading order, or a contrast pairing only exists once the parts are together. A View assembled by an agent is where that shows up.
They are warnings, so an ordinary run stays green and only --strict fails on
them. Install axe-core alongside Playwright to enable the pass; without it,
rendering proceeds and the audit is skipped.
npm i -D playwright axe-coreaxe runs through the debugger protocol rather than as page script, so it reaches
the View without the CSP granting unsafe-eval. The audit sees the same
document a host would render, under the same restrictions.
Transitional builds without a manifest can pass one or more HTML files:
npx mountly-mcp verify --html dist/first.html --html dist/second.htmlUse either --manifest or --html, not both.
Programmatic conformance
Section titled “Programmatic conformance”The CLI and test runners share the same engine:
import { formatConformanceReport, verifyMcpApps } from "mountly-mcp/testing";
const report = await verifyMcpApps({ manifestPath: "dist/mountly-mcp.manifest.json", render: true,});
console.log(formatConformanceReport(report));if (!report.ok) process.exitCode = 1;report.diagnostics contains stable codes, severities, messages, and optional
source paths for custom CI output. report.mode is "static" or "browser",
so a dashboard can state which tier produced a pass instead of inferring it
from the options it passed:
if (report.mode === "static") { console.warn("no runtime coverage: rerun with render: true");}Recommended release gate
Section titled “Recommended release gate”{ "scripts": { "build:mcp": "mountly-mcp build", "verify:mcp": "mountly-mcp verify --render --strict" }}Run a final smoke test in each real host you support. Local conformance cannot prove host-specific theme values, available display modes, viewport policy, or surrounding product UI.