sema.run now exposes 17 typed tools through WebMCP. A browser agent can read and replace the editor, format and run Sema code, inspect output, find examples, manage files in the WASM virtual filesystem, set breakpoints, and step through the debugger without clicking through the interface.
I added it to see what a real agent API for the playground would require. Calling the playground from outside its UI quickly reached the editor, worker runtime, virtual filesystem, and debugger. Each action needed clear inputs, a stable completion state, and an error path. Several did not have all three.
WebMCP Is an API Inside the Page
WebMCP lets a web page publish JavaScript functions as tools for browser
agents. Each tool gets a name, a description, a JSON Schema for its arguments, and an execute callback. The current
imperative API hangs off document.modelContext. This is the registration function from playground/src/webmcp.js:
export async function registerPlaygroundWebMcp(actions = {}) {
const modelContext = document.modelContext;
if (!modelContext || typeof modelContext.registerTool !== "function") return null;
const controller = new AbortController();
try {
const registrations = TOOL_DEFINITIONS.map((definition) =>
modelContext.registerTool(
{
...definition,
execute: async (input = {}) => {
const action = actions[definition.name];
if (typeof action !== "function") {
return failure("NOT_READY", "The Sema playground is still initializing.");
}
try {
return await action(input);
} catch (error) {
return failure(error?.code ?? "INTERNAL_ERROR", error?.message ?? String(error));
}
},
},
{ signal: controller.signal },
),
);
await Promise.all(registrations);
} catch (error) {
controller.abort();
throw error;
}
return controller;
}
sema mcp already gives an agent a headless process with tools for evaluating files, compiling programs, searching
documentation, and operating notebooks. WebMCP acts inside the open browser tab, using the editor contents, output
panel, virtual files, debugger state, and session the person can already see.
ARD handles discovery. It tells an agent that Sema has an MCP server, agent guide, documentation index, and browser tools. WebMCP lets the agent operate the page after it arrives.
The Playground Became the Interface
Registration lives in a 192-line adapter that receives an action map from app.js. The UI and the agent call the same
functions, so formatting code through WebMCP follows the same path as pressing Format. Running code follows the same
worker runtime as pressing Run. Debugging waits on the same state transitions as the debugger controls.
The adapter registers these 17 tools:
- Editor and runtime:
read_editor,write_editor,format_editor,run_editor,stop_run, andread_output - Examples:
find_examplesandload_example - Virtual filesystem:
list_files,read_file, andwrite_file - Debugger:
set_breakpoints,start_debugging,continue_debugging,step_debugger,stop_debugging, andget_debug_state
The surface excludes file deletion, clearing the filesystem, uploading host files, and switching storage backends. File access stays inside the playground's WASM virtual filesystem.
read_editor, read_file, and read_output paginate their results. Virtual file writes are limited to 1 MiB and
reject ambiguous paths, .., backslashes, and NUL bytes. Mutating tools return BUSY while evaluation or debugging is
in flight. Source code, output, files, and debugger values are marked as untrusted content so the browser agent knows
not to treat text produced by a program as instructions.
The tools register before the WASM runtime finishes loading, making them immediately discoverable. Calls made too early
return a structured NOT_READY error. Unsupported browsers take the normal playground path. All registrations share one
AbortController; if Chrome rejects one tool, the adapter removes the rest.
The Tools Found Runtime Bugs
The first focused WebMCP tests passed. The next sweep covered every tool action, the unsupported-browser path, and whether Run was actually using its Web Worker. That sweep went below the tool definitions and into the runtime.
Worker initialization could hang forever. A worker crash could leave the Run button disabled and the status stuck on
Running. The fix added a ten-second initialization timeout, rejected pending calls when the worker died, restored the
controls, and sent the next evaluation through the main-thread fallback. It does not replay the failed evaluation: the
worker may already have printed output or modified the virtual filesystem, so running it again could duplicate side
effects.
The debugger exposed a second problem. The Rust/WASM runtime contained newer promise-based debugger methods, but the
generated browser package did not export them and the live UI still used an older poll-and-replay loop. A WebMCP call
such as step_debugger had to resolve after the debugger paused, finished, or failed. That requirement pushed the UI
onto the promise API too.
The promise migration exposed three more bugs. A cooperative Yielded result was treated as fatal instead of resumable.
Captured stderr could arrive tagged as stdout. A successful fallback evaluation could return 42 while the status bar
still said Error.
By the end of the sweep, the browser suite passed 109 tests with one intentional skip. The async runtime passed 170 tests, and the VM passed 545. The WebMCP test file covers the exact tool surface, schemas, annotations, unsupported browsers, editor and output paging, example loading, virtual path rejection, debugger stepping, concurrent mutation, worker cancellation, and both synchronous and asynchronous registration failures.
A button can start work and return control to the browser. A tool call needs a stable result. Sharing the actions forced the worker and debugger to define exactly when each operation completed or failed.
The First Agent Run Hit a Rate Limit
After deployment, I connected the WebMCP Inspector and asked its Gemini agent to compare how long the visual examples took to run against the filesystem examples, including which programs modified the filesystem.
The agent discovered all 17 tools. It searched examples, listed files, and loaded code. After eight successful model-and-tool round trips it had run one real program, in 117.55 milliseconds, when Gemini returned an HTTP 429.
Comparing 18 examples required repeatedly loading, running, timing, and inspecting filesystem state. list_files was
non-recursive, state accumulated between runs, and a single timing was not a useful benchmark anyway. The 17 primitives
were the wrong size for this job.
A benchmark_examples tool would run selected examples in isolated filesystems, perform warmups, record median timings,
and return a filesystem diff in one call.
Making It Discoverable
The WebMCP work shipped alongside a canonical ai-catalog.json on
sema-lang.com. It advertises four resources: the native Sema MCP server, the agent coding guide, the generated
llms.txt documentation index, and the playground's WebMCP surface. Both sema-lang.com and sema.run point agents to
that catalog through HTML and robots.txt. The sema.run llms.txt explains the browser
workflow.
I checked the live discovery path with ardvark, the ARD crawler and verifier I built earlier:
$ ardvark probe sema.run
miss sema.run well_known 404
hit sema.run robots_agentmap found 200
probe complete: 1 hit · 1 miss · 0 errors
$ ardvark verify https://sema-lang.com/.well-known/ai-catalog.json
✓ identifier.unique
✓ catalog.spec_version
✓ urn.publisher_matches
✓ queries.count
✓ entry.media_type
... 22 checks passed
verdict: valid
The 404 is intentional. The only catalog lives on sema-lang.com; sema.run points to it through the Agentmap directive.
Chrome 150's production Lighthouse run detected all 17 tools. Tool registration, JSON Schemas, the accessibility tree,
and llms.txt each scored 1.0. The overall experimental Agentic Browsing score was 0.98, with the remaining deduction
coming from ordinary layout shift.
WebMCP is still experimental. The API had already moved from navigator.modelContext to document.modelContext by the
time this implementation shipped, and sema.run currently enables it through a Chrome origin-trial token. The adapter is
feature-detected and isolated, so the rest of the playground does not depend on it.
The API can move again without undoing the runtime cleanup. Sema now has explicit operations for the editor, worker runtime, virtual filesystem, and debugger. The UI and WebMCP share them.
Try the tools at sema.run. The implementation is in
playground/src/webmcp.js, with the full source
at github.com/sema-lisp/sema. For the earlier Sema story, read
Building Sema and
Sema After the First Week.
