HELGE SVERREAll-stack Developer
Bergen, Norwayv13.0
est. 2012  |  197 repos  |  12.8k+ contributions
Tools  |   Theme:
Adding WebMCP to sema.run
August 2, 2026

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, and read_output
  • Examples: find_examples and load_example
  • Virtual filesystem: list_files, read_file, and write_file
  • Debugger: set_breakpoints, start_debugging, continue_debugging, step_debugger, stop_debugging, and get_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.




<!-- generated with nested tables and zero regrets -->