Skip to main content

Chapter 23 · Part III · In practice

WebMCP: an agent at work in the Sandbox

What an AI agent in the browser can do with the Sandbox through its 36 WebMCP tools — look at the pages, change any setting, edit the text, add pictures, tables and fonts, run the checks, export — and how to call each one, with real arguments and answers

Updated 2026-10-1314 minenescaptzhjaar

In short

This page is for people who want an AI helper to work on a book in the Sandbox for them. While the Sandbox is open, it gives the helper a set of tools: to look at any page, change the design, edit the text, add pictures and tables, find problems and save files. You ask in your own words, for example "make the text a little larger and check the book still fits in 40 pages", and the helper uses the tools and shows you the pages. Everything stays in your browser. The page explains how to connect a helper, what it can do and how each tool is called.

An agent in your browser works on the book open in the Sandbox: it looks at the pages, changes them and looks again.

The Sandbox gives an agent its own tools through WebMCP, a draft web standard that lets a page hand tools to the AI model the browser talks to. With them the agent does not click through the interface: it reads the book, asks for a picture of page 12, sets bodyText.fontSize to 10.5 pt, sees that the chapter went from 18 to 19 pages, and puts it back. It does what you do by eye, and it does it with your book, in your browser.

There are 36 tools whose names start with sandbox_, registered while the Sandbox is on screen, and 8 tools of the site, site_*, on every page of postext.dev. This page sorts them by what you would ask an agent to do, and gives the call for each one.

#Connect an agent

#In a browser with WebMCP

The tools are registered with document.modelContext when the Sandbox mounts and taken back when you leave it. An agent the browser hands page tools to finds them there. Nothing has to be installed or set up on the Postext side.

WebMCP is still a draft. Chrome offers it as an origin trial, and behind the flag chrome://flags/#enable-webmcp-testing. The tools follow the page: on the home page or in the docs the agent has the site_* tools; once the Sandbox is on screen, the 36 sandbox_* tools appear as well.

#In any other browser: window.postextWebMcp

The page also puts the same tools on window.postextWebMcp, with or without WebMCP. Any agent that can run script in the page can use them: an automation tool such as Puppeteer or Playwright, an MCP server that drives the browser and evaluates script (Chrome DevTools MCP), a browser extension, or you, in the developer console:

// What is on this page
postextWebMcp.listTools().map((t) => t.name);
 
// Run one tool: the name, then an object with its arguments
await postextWebMcp.executeTool('sandbox_get_state', {});
await postextWebMcp.executeTool('sandbox_set_config', {
  changes: [{ path: 'bodyText.fontSize', value: '10.5pt' }],
});

The same from a Puppeteer script, which saves a page as a picture:

import fs from 'node:fs';
import puppeteer from 'puppeteer';
 
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://postext.dev/en/sandbox');
await page.waitForFunction(() => window.postextWebMcp?.listTools().some((t) => t.name === 'sandbox_get_state'));
await page.waitForFunction(async () => (await window.postextWebMcp.executeTool('sandbox_get_state', {})).ready);
 
const result = await page.evaluate(() =>
  window.postextWebMcp.executeTool('sandbox_render_page', { chapter: 1, position: 1, width: 1200, format: 'png' }),
);
const picture = result.content.find((part) => part.type === 'image');
fs.writeFileSync('page-1.png', Buffer.from(picture.data, 'base64'));
await browser.close();

A fresh browser profile has its own storage, so it starts from the samples, not from your projects. To work on your own books, use the browser you made them in.

#What every answer looks like

A tool answers with a JSON object. ok: true and what was asked, or ok: false and an error that says what was wrong and what to ask instead:

{
  "ok": false,
  "error": "bodyText.fontSize is a length: give { \"value\": 11, \"unit\": \"pt\" }, a number (in pt) or a text such as \"11pt\". The units: cm, mm, in, pt, px, em, rem."
}

The arguments are checked against the tool's schema before anything runs, so a misspelt parameter is refused with the list of the right ones. A tool that returns pictures (sandbox_render_page) answers in the MCP shape, { content: [...] }: a text part with JSON describing the pages, then one image part per page, base64-encoded.

#The working loop

Every tool that changes the book waits for the layout to follow before it answers, and says how many pages each chapter came to. So the next look is always at the new pages, and the agent can work as a designer does:

  1. See where things stand: sandbox_get_state gives the book, its chapters with their pages, the chapter and page on screen and the open panel.
  2. Look: sandbox_render_page paints a page, sandbox_inspect_pages describes it as data, sandbox_get_warnings lists what the checks found.
  3. Change: sandbox_set_config, sandbox_edit_text, sandbox_save_resource…
  4. Look again, and keep the change or take it back.

A page is laid out and painted for the tool whatever viewer is on screen, so the agent can look at chapter 7 while you read chapter 2.

#What you can ask for, and how it is done

#Find out what is open

// sandbox_get_state
{}
{
  "ok": true,
  "ready": true,
  "book": { "kind": "sample", "id": "postext-guide", "name": "Postext guide" },
  "pages": 3,
  "chapters": [
    { "chapter": 1, "id": "a", "title": "Opening", "onScreen": true, "words": 1007, "pages": 2 },
    { "chapter": 2, "id": "b", "title": "Second", "words": 101, "pages": 1 }
  ],
  "view": { "viewer": "canvas", "panel": "none", "scope": "chapter", "chapter": 1 },
  "resources": 0,
  "warnings": 0
}

ready: false comes with waitingFor, which says why (a book still loading). question and notice appear when the Sandbox is waiting on you; see Questions and notices.

Every tool that takes a chapter accepts its position in the book (from 1), its title or its id. Left out, it is the chapter on screen.

#Look at a page

A picture. "Show me the opening of chapter 3", "how does the spread on pages 24 and 25 look?":

// sandbox_render_page: a spread, 1200 px wide per page
{ "chapter": 3, "page": 24, "count": 2, "width": 1200 }

page is the number printed on the page; position is the page's place among the chapter's pages (from 1), for pages without a number. Without either, it is the page on screen. crop magnifies a part of the page, in fractions of its width and height from the top left corner, to judge type and spacing up close:

// sandbox_render_page: the bottom right quarter, magnified
{ "chapter": 3, "page": 24, "crop": { "x": 0.5, "y": 0.5, "width": 0.5, "height": 0.5 }, "width": 1600 }

printPreview: true paints the soft proof of the print profile with the trim, bleed and safe-zone guides; trimmed: true leaves out the margin with the crop marks.

The same page as data. "Which lines on this page are loose?", "is the second column full?":

// sandbox_inspect_pages: without a page, one line per page of the chapter
{ "chapter": 1 }
{
  "chapter": 1, "title": "Opening", "chapterPages": 2, "converged": true,
  "pages": [
    { "position": 1, "label": "1", "role": "opener", "blank": false, "lines": 45, "figures": 0, "starts": "Opening" },
    { "position": 2, "label": "2", "role": "body", "blank": false, "lines": 37, "figures": 0, "starts": "decades. Print typography spent five centuries…" }
  ]
}

With page or position it lists every block of the page: its type, column, text, number of lines, the lines that end in a hyphen and the loose justified lines with how wide their spaces are, its box on the page, plus how full each column is (fill: 0.99).

What is at a point. When the agent sees something in a picture, it asks what it is with the same fractions it used for the crop:

// sandbox_inspect_point
{ "chapter": 3, "page": 24, "x": 0.71, "y": 0.42 }

The answer gives the block, the line and column of the Markdown source there (and the word), a link and the page it leads to, the table cell or caption of a resource. With select: true it also does what a click does: the Manuscript panel opens with the caret at that place (word: true selects the word, to selects up to a second point).

#Change the design

Any setting of the configuration can be changed by its path. An agent rarely knows the path at first, so it looks for it:

// sandbox_find_setting
{ "query": "orphans" }
{
  "ok": true,
  "paths": [
    { "path": "bodyText.avoidOrphans", "effective": true },
    { "path": "bodyText.avoidOrphansInLists", "effective": true }
  ],
  "sections": [{ "section": "Body text", "group": "Typography", "settingsGroup": "text", "configKeys": ["bodyText"] }]
}

sandbox_get_config reads a setting or a whole section: set is what the book stores, effective what it comes to with the defaults filled in, which also shows every setting the section has.

// sandbox_get_config
{ "path": "headings.levels[level=2]" }

Paths name an item of a list by what tells it apart: headings.levels[level=2].fontSize, header.elements[id=titleOdd].italic, calloutStyles[id=note].title. Then sandbox_set_config changes as many settings as you like in one call:

// sandbox_set_config
{
  "changes": [
    { "path": "bodyText.fontSize", "value": "10.5pt" },
    { "path": "page.margins.top", "value": { "value": 2.5, "unit": "cm" } },
    { "path": "headings.levels[level=2].color", "value": "#1f3a5f" },
    { "path": "bodyText.avoidOrphans", "value": true }
  ]
}

A length is "2.5cm", { "value": 2.5, "unit": "cm" } or a number in the setting's unit; a colour is "#1f3a5f". A null value removes the setting, so the design's value comes back. The answer is how to judge the change, and how to take it back:

{
  "ok": true,
  "changed": [
    {
      "path": "bodyText.fontSize",
      "from": null,
      "to": { "value": 10.5, "unit": "pt" },
      "effective": { "from": { "value": 10, "unit": "pt" }, "to": { "value": 10.5, "unit": "pt" } }
    }
  ],
  "followed": [
    { "path": "captionStyle.fontSize.value", "from": 10, "to": 10.5 },
    { "path": "tableStyle.bodyFontSize.value", "from": 10, "to": 10.5 }
  ],
  "pagesBefore": 212,
  "pagesAfter": 219,
  "chapters": [{ "chapter": 1, "title": "Opening", "pages": 19, "pagesBefore": 18 }]
}

from is the stored value before (here null: the book did not set it), so the same call with from as the value undoes it. followed lists the settings that depend on this one and moved with it. A note appears when the engine resolved something other than what was asked (a limit, a setting that depends on another), and a value of the wrong shape, or a configuration the engine cannot resolve, is refused with nothing changed.

For larger changes, patch merges an object into the configuration (objects merge, null removes, lists are replaced whole) and replace puts a whole configuration in place of the book's, such as a postext-config.json export. sandbox_reset_to_original with part: "config" puts back the design the book started from.

Fonts are settings too: sandbox_list_fonts with search: "Garamond" finds families in the open catalogue, and writing the name into a fontFamily setting loads it.

#Edit the text

The text of each chapter is Markdown with Postext's extensions (see Document format). The agent reads it with line numbers, finds a passage, and replaces exact passages:

// sandbox_read_text: lines 40 to 80 of chapter 2
{ "chapter": 2, "fromLine": 40, "toLine": 80 }
// sandbox_search_text: where a passage occurs, and on which page
{ "query": "baseline grid", "pages": true }
// sandbox_edit_text
{
  "chapter": 2,
  "edits": [
    { "find": "The last words of the opening.", "replace": "The closing words of the opening." },
    { "find": "colour", "replace": "color", "all": true }
  ]
}

Each find must match the source exactly and occur once, unless all is set. Either every edit applies or none does: a passage that is not there is refused with the advice to read the chapter again. The whole call is one step for sandbox_undo_text, which takes it back (redo: true puts it back). sandbox_write_text replaces a whole chapter, from markdown or from a .md file.

To show you a place in the text, sandbox_select_text opens the Manuscript panel with that passage selected (by text, line or offset), as a click on a finding of the Checks panel does.

#Organise the chapters

sandbox_manage_chapters adds, renames, moves, splits, merges and shows chapters:

// add a chapter in third place
{ "action": "add", "title": "Epilogue", "markdown": "# Epilogue\n\nThe end.\n", "position": 3 }
 
// split a chapter where a passage starts
{ "action": "split", "chapter": "Opening", "before": "## Second part" }
 
// one chapter per level-1 heading
{ "action": "split_at_headings", "chapter": 1 }

sandbox_remove_chapter deletes one, which cannot be undone.

#Add pictures, tables, videos and fonts

sandbox_save_resource adds a resource or changes one, as the Resources panel does. A file comes by url (an https: address the page can fetch, or a data: URL) or as base64:

// a picture with its caption and alternative text
{
  "id": "harbour",
  "image": { "url": "https://example.org/harbour.jpg" },
  "caption": "The harbour at dawn",
  "altText": "Fishing boats moored at a stone quay, mist over the water",
  "placement": { "position": "top", "span": "page" }
}
// a table from rows of text; the first row is the header
{
  "id": "prices",
  "caption": "Prices in 1850",
  "table": { "rows": [["Item", "Price"], ["Bread", "2 d"], ["Ink", "6 d"]], "columnWidths": [2, 1] }
}
// a YouTube video: its poster, frame size and title are fetched
{ "id": "interview", "video": { "url": "https://www.youtube.com/watch?v=…" }, "caption": "The author at work" }

The answer gives the stored record and the line that places it, ::resource{id="prices"}, which the agent then writes into the text with sandbox_edit_text. On an existing id the call changes only what it gives: a caption, a table's cells, the placement, a picture's safe area. newId renames a resource and, by default, every reference to it in the book. sandbox_list_resources lists them, and with an id gives one in full (a table's cells with their formatting, an SVG's source).

Fonts uploaded into the book go through sandbox_save_font:

{ "action": "add", "family": "My Serif", "weight": 700, "style": "italic", "file": { "url": "https://example.org/MySerif-BoldItalic.woff2" } }

#Check the book

// sandbox_get_warnings: every chapter
{ "scope": "book" }

The answer is what the Checks panel says, for any chapter or the whole book whatever is on screen: missing fonts and pictures, markup the engine could not read, design elements that do not fit, text too faint for its background, loose lines, widows, overflowing boxes and the print preflight. Each warning has its category, chapter, line and offsets, its page, and the setting or resource it names:

{
  "ok": true,
  "scope": "book",
  "total": 3,
  "byCategory": { "typesetting": 2, "design": 1 },
  "warnings": [
    {
      "kind": "headerFooterMetadataMissing",
      "category": "design",
      "title": "Placeholder metadata missing",
      "detail": "header · {title} — The metadata referenced by this placeholder is not set — it will render empty."
    }
  ]
}

From a warning the agent can go to its place (sandbox_select_text with offset and toOffset, or sandbox_go_to_page), look at the page, and change the text or the setting. category narrows the list; on a long book scope: "book" lays every chapter out and takes a while.

#Show the reader

What the agent changes, you see. To point you at something:

// sandbox_set_view: the Canvas with facing pages and loose lines highlighted
{ "viewer": "canvas", "pages": "spread", "fit": "height", "guides": { "looseLines": true, "baselineGrid": true } }
// sandbox_go_to_page: where a figure is set
{ "anchor": "harbour" }

sandbox_set_view also chooses the viewer (canvas, pdf, folio, html, epub), the side panel, chapter or whole book, zoom, the reader's type size, the print preview and the Design panel's group. sandbox_go_to_page turns pages by step, page, position, a text passage, an anchor or an EPUB's contents entry. sandbox_run_viewer presses a toolbar button: regenerate writes the PDF or EPUB again and waits for it, print opens the print dialog for you to confirm, play_video plays a video on a page.

#Comic pages

sandbox_inspect_comic gives the panels, split lines and balloons of a comic page with their positions; sandbox_edit_comic makes the edits a person makes with the pointer, written into the source:

// move a balloon's centre to a point of its panel's picture, in percent
{ "comic": 1, "action": "move_balloon", "balloon": 2, "at": { "x": 30, "y": 18 } }
 
// move the first split line to 45 % of its cell
{ "comic": 1, "action": "move_splitter", "splitter": 1, "start": 45 }

The answer warns when a balloon lands on a face or runs out of its panel. See Comics for the format.

#Books, files and Word

  • Open and keep books. sandbox_list_books lists the samples and your projects; sandbox_open_book opens one ({ "kind": "sample", "id": "postext-guide", "language": "es" }); sandbox_save_book with action: "copy_current" keeps the book on screen as your own project, new_blank starts an empty one.

  • Export. sandbox_export writes pdf, epub, postext, docx, markdown or config. By default the browser downloads the file; with deliver: "data" a PDF or an EPUB comes back base64-encoded to the agent:

    { "format": "pdf", "scope": "book", "deliver": "data" }
  • Import a .postext file with sandbox_import_book (inspect: true says which languages it carries without importing it).

  • Bring in a Word manuscript with sandbox_import_word. Run it first with dryRun: true: it changes nothing and answers with the quality report, each Word style with what it would become, and the chapters and resources it would make. Then correct the mapping and run it for real:

    {
      "file": { "url": "https://example.org/manuscript.docx" },
      "destination": "book",
      "mapping": { "paragraphs": { "Epigraph": { "kind": "quote" }, "Chapter Title": { "kind": "chapter" } } },
      "saveTemplateAs": "My publisher"
    }

    sandbox_word_templates lists, reads, saves and deletes the saved mappings, which the Word export uses too.

#Questions and notices

When a link opens a book you already have your own version of, the Sandbox asks whether to replace your copy, and sandbox_get_state reports it as question with ready: false. sandbox_answer answers it: replaceCopy: false keeps yours, true replaces it with the published book and deletes your copy. dismissNotice: true clears the message of the Books panel.

#Three sessions from start to finish

"Make the text a little larger, but keep the book under 220 pages." sandbox_get_state (212 pages) → sandbox_set_config with bodyText.fontSize at "10.5pt" (219 pages) → sandbox_render_page on a dense page to judge it → sandbox_get_warnings with category: "typesetting" for new widows → if something went wrong, sandbox_set_config again with the from values of the first answer.

"Fix what the checks found in chapter 4." sandbox_get_warnings with chapter: 4 → for each warning, sandbox_read_text around its line and sandbox_render_page of its page → sandbox_edit_text or sandbox_set_config → sandbox_get_warnings again until it comes back empty.

"Make a book from this Word file and give me the PDF." sandbox_save_book with action: "new_blank" → sandbox_import_word with dryRun: true → corrected mapping, then the real import with destination: "book" → sandbox_get_warnings with scope: "book" → sandbox_export with format: "pdf" and scope: "book".

Six tools do what cannot be taken back, and are marked as consequential (consequentialHint), so the browser can ask you before they run: sandbox_remove_chapter, sandbox_delete_resource, sandbox_delete_book, sandbox_reset_to_original, sandbox_import_word (it can replace the whole book) and sandbox_answer (it can delete your copy of a book). Ask the agent to export a .postext copy first when in doubt.

Tools that only read are marked readOnlyHint, and those whose answer carries text the page did not write (the book's own text, a fetched file) are marked untrustedContentHint, so the browser can keep that text from being taken for instructions.

#Limits

  • Everything stays in your browser. The tools reach nothing the Sandbox itself does not: your books are in this browser's storage, and a file given by url is fetched by the page, so its server must allow it (CORS). A data: URL or base64 always works.
  • Long work takes time. A tool waits for the layout before it answers; wait: false answers at once. A PDF or EPUB of a long book takes minutes.
  • What sandbox_undo_text takes back: the text edits of the tools (one call at a time) and the edits made on the previews. Not what you type in the Manuscript editor (its own undo does that), chapters added or moved, or changes to the design and the resources: for those, the from of each answer. The list is lost when the page is reloaded.
  • The book on screen. The tools work on the open book. Only sandbox_export of a .postext file, sandbox_save_book with duplicate and sandbox_reset_to_original with sample reach another book without opening it.

#The tools of the site

On every page of postext.dev, Sandbox included:

  • site_get_context: the page on screen, its language and theme, the sections and every page of the documentation.
  • site_search: the docs and the Cookbook, by words, setting names (bodyText.avoidOrphans) or directives (::resource).
  • site_read_page: a page of the site as Markdown, without leaving the page on screen.
  • site_open_page: show a page, with #heading to land on a section.
  • site_list_recipes and site_open_recipe_in_sandbox: find a Cookbook recipe and open it in the Sandbox as a book you can edit.
  • site_lookup_term: a term of the glossary (widow, kerning, kinsoku…).
  • site_set_preferences: the site's language and theme.

An agent that starts on the home page can therefore search the Cookbook, open a recipe in the Sandbox and go on there with the sandbox_* tools.

#Every tool

Grouped as the Sandbox registers them. The full description and schema of each one come with postextWebMcp.listTools().

  • What is open: sandbox_get_state.
  • The pages: sandbox_render_page, sandbox_inspect_pages, sandbox_inspect_point.
  • The text: sandbox_read_text, sandbox_write_text, sandbox_edit_text, sandbox_undo_text, sandbox_select_text, sandbox_search_text, sandbox_manage_chapters, sandbox_remove_chapter.
  • The design: sandbox_get_config, sandbox_set_config, sandbox_find_setting, sandbox_reset_to_original.
  • How it is shown: sandbox_set_view, sandbox_go_to_page, sandbox_run_viewer.
  • Resources and fonts: sandbox_list_resources, sandbox_save_resource, sandbox_delete_resource, sandbox_list_fonts, sandbox_save_font.
  • Comics: sandbox_inspect_comic, sandbox_edit_comic.
  • Books and files: sandbox_list_books, sandbox_open_book, sandbox_save_book, sandbox_delete_book, sandbox_import_book, sandbox_export.
  • Word: sandbox_import_word, sandbox_word_templates.
  • Questions: sandbox_answer.
  • The checks: sandbox_get_warnings.

#When you embed the Sandbox

<PostextSandbox> registers the tools while it is mounted; webmcp={false} leaves them out (see Props). A host page can add tools of its own through the same module:

import { registerWebMcpTools } from 'postext-sandbox/webmcp';
 
const controller = new AbortController();
registerWebMcpTools([
  {
    name: 'shop_list_titles',
    description: 'The titles of this shop, with the id of the Postext project of each one.',
    inputSchema: { type: 'object', properties: {}, additionalProperties: false },
    annotations: { readOnlyHint: true },
    execute: async () => ({ titles: await fetchTitles() }),
  },
], controller.signal);
// controller.abort() takes them back

They are registered with WebMCP where the browser offers it and always on window.postextWebMcp, with their arguments checked against inputSchema.