Subchapter 1.1
references/implementation-and-validation.mdMarkdown8 KBView on GitHub
Read this reference while analyzing a target repository, choosing integration points, or configuring the Stagehand validator.
Build a compact table before editing:
| Capability | UI entry | Handler/service | Validator | Auth/guard | Side effect | Risk | Proposed tool/test |
|---|
Trace real call paths. A route name or schema alone does not prove that a capability is usable, authorized, or safe to expose.
Framework signals worth checking:
app/**/page.*, route.*, files containing "use server", client components, server actions, and root layouts/providers.pages/**, pages/api/**, _app.*, API clients, and form handlers.<Route>, createBrowserRouter, route modules, mutation/query hooks, and the root render entry.<form>, submit listeners, named task functions such as search*, calculate*, or save*, network clients and query/mutation hooks, RPC routers, OpenAPI/JSON Schema, validation libraries, authorization middleware, local persistence or state stores (including localForage), and existing modelContext usage.The bundled scanner reports file/line leads without printing source snippets. Open only the relevant files and follow imports and calls manually.
Use imperative registration when the application already has a typed client or service function and the tool should return a structured result without UI navigation.
Use declarative form integration when a visible form is the source of truth and user review is part of the intended flow. Current experimental Chromium builds recognize attributes such as:
<form toolname="search_catalog" tooldescription="Search the public product catalog.">
<input
name="query"
required
toolparamdescription="Words in the product name or description"
/>
<button type="submit">Search</button>
</form>Do not add automatic submission to payment, message sending, publishing, deletion, permission changes, or other consequential forms.
Keep the definitions next to the existing client boundary or in a small client-only module imported by the root provider:
const modelContext = navigator.modelContext || document.modelContext;
if (modelContext?.registerTool) {
await modelContext.registerTool({
name: "search_catalog",
description: "Search products visible to the current user without changing application state.",
inputSchema: {
type: "object",
properties: {
query: { type: "string", minLength: 1 },
limit: { type: "integer", minimum: 1, maximum: 20 },
},
required: ["query"],
additionalProperties: false,
},
annotations: {
readOnlyHint: true,
untrustedContentHint: true,
},
execute: async (input) => {
const parsed = SearchCatalogInput.parse(input);
const response = await searchCatalog(parsed);
return {
results: response.items.map(({ id, name, price }) => ({ id, name, price })),
};
},
});
}WebMCP browser APIs are still evolving. Check the browser and project versions before choosing cleanup behavior. Register once at the application root; use supported abort/unregister behavior when present, and guard development hot reload from duplicate names. Do not guess an unregistration signature.
The registration API currently accepts readOnlyHint and untrustedContentHint. Stagehand v4 exposes their discovered values as tool.annotations.readOnly and tool.annotations.untrustedContent; declarative tools may also expose tool.annotations.autosubmit. Keep this registration-versus-discovery naming distinction in tests.
additionalProperties: false, configure the execute-time validator to reject unknown keys rather than silently stripping or accepting them.The validator expects JSON shaped like:
{
"timeoutMs": 5000,
"expectedDom": [
{
"selector": "#last-tool",
"text": "search_catalog"
}
],
"tools": [
{
"name": "search_catalog",
"risk": "read-only",
"expectedAnnotations": {
"readOnly": true,
"untrustedContent": true
},
"input": {
"query": "notebook",
"limit": 3
},
"expectedOutputSubset": {
"results": []
}
},
{
"name": "submit_order",
"risk": "consequential"
}
]
}Rules:
input is discovery-only.input is invoked and must finish with expectedStatus (default Completed).expectedAnnotations and expectedOutputSubset are recursive subset assertions.expectedDom checks exact textContent after all configured invocations, proving the page observed the calls.risk must be read-only, reversible, or consequential.--allow-consequential is supplied. Supply it only for an explicitly authorized sandbox with synthetic data.Local application validation:
node scripts/validate-stagehand.mjs \
--url http://127.0.0.1:3000 \
--config /path/to/webmcp.e2e.json \
--localIf Chrome is installed outside the platform default locations, add --executable-path /path/to/chrome. In an isolated CI container that cannot launch Chrome’s sandbox, add --no-sandbox; do not use that flag on an ordinary workstation.
Public preview validation on Browserbase:
node scripts/validate-stagehand.mjs \
--url https://preview.example.test \
--config /path/to/webmcp.e2e.json \
--browserbaseUse --init-script /path/to/register.js only to validate a generated registration module or the validator fixture. Run the final target proof without it.
The Browserbase-owned Stagehand eval site is a stable no-injection check for the validator itself:
pnpm test:e2e:ownedIt discovers all four tools published by https://browserbase.github.io/stagehand-eval-sites/sites/webmcp-test/, invokes two deterministic read-only tools, and leaves the failure and support-submission tools discovery-only. Purpose-built catalog and support gym configurations live under tests/fixtures/; run them against their local eval-site checkout before publishing changes.