A dependency-free pixel canvas for browser agents. The page exposes six WebMCP tools; an external agent decides which colors belong at which coordinates, while the app validates, stores, and visibly renders the result.
npm run serveOpen http://localhost:8080. The canvas still works as a viewer in browsers
without WebMCP. To use site tools, open it in a compatible browser agent or a
Chromium build with WebMCP enabled.
No install or build step is required. Run the deterministic suite with:
npm test-
Click and drag on the canvas to paint a different random color into each visited pixel. Fast movement fills intervening cells; revisiting a cell in the same drag keeps its color. A stroke previews immediately, then saves as one undoable change on release, cancellation, or focus loss. At 4,096 pixels the stroke saves automatically; begin another drag to continue.
-
Painting shares the edit lease with WebMCP, so an agent cannot overwrite an active mouse stroke and mouse painting waits while an agent holds the lock. Preview pixels are not included in inspection until the stroke is saved.
-
The first canvas is 51×51 and can grow to any larger odd size through 111×111.
-
(0,0)is the center.xincreases right andyincreases up. -
Colors use
#RGBor#RRGGBB; the app normalizes them to uppercase#RRGGBB. -
Missing pixels are transparent state displayed with the active theme's canvas color.
-
The grid is visible on first visit. The corner button toggles it, and the preference is saved locally.
-
Light mode is the first-visit default. The adjacent sun/moon button switches themes and saves that preference separately from artwork.
-
The trash button clears every pixel on the canvas (undoable via the undo snapshot).
-
Canvas content, revision, and one undo snapshot are stored under
pixel-generator:v1inlocalStorage.
| Tool | Purpose |
|---|---|
inspect_pixels |
Reads canvas context, exact paginated pixels, or changes since an available revision. Pixel pages use the smallest lossless encoding and continuation pages omit repeated metadata. |
manage_edit_session |
Acquires, refreshes, or releases the five-minute single-agent edit lease. |
generate_pixels |
Places or replaces up to 4,096 unique pixels atomically using coordinate groups or a mix of palette runs, indexed rows, and rectangles. |
clear_pixels |
Clears selected coordinates or every colored pixel atomically. |
increase_canvas_size |
Grows the centered canvas without moving existing coordinates. |
undo_last_change |
Reverts the latest successful canvas mutation without providing redo. |
All mutations require the opaque edit-session token and exact current revision. A stale call fails without partial changes. Successful mutations return after the canvas repaints and use this shared result shape:
{
"ok": true,
"tool": "generate_pixels",
"revision": 4,
"canvasSize": 51,
"affectedPixels": 120,
"undoAvailable": true
}Recommended agent flow:
- Call
inspect_pixels. - Acquire the lock with
manage_edit_sessionand the inspected revision. - Call one or more mutation tools, carrying forward each returned revision.
- Inspect a bounded region when exact verification is needed.
- Release the edit session, including after a recoverable failure.
For compact artwork, generate_pixels accepts an ordered color palette and
one or more palette primitives:
- Horizontal run:
[y,xStart,xEnd,paletteIndex]with inclusive endpoints. - Indexed row:
[y,xStart,[paletteIndex,...]]for detailed contiguous rows. - Filled rectangle:
[xMin,yMin,xMax,yMax,paletteIndex]with inclusive bounds.
The primitives can be combined, but their expanded coordinates must not overlap. Coordinate groups remain available as a separate compatibility mode.
{
"sessionToken": "token-from-manage-edit-session",
"expectedRevision": 4,
"runPalette": ["#EF4444", "#3B82F6"],
"runs": [[1, -3, 3, 0]],
"indexedRows": [[0, -1, [1, 1, 1]]],
"rectangles": [[-5, -5, 5, -4, 0]]
}inspect_pixels({"includePixels":true}) compares exact coordinate-group,
palette-row-run, and palette-indexed-row representations and returns the
smallest. Its first page includes canvas context; calls with nextCursor
return only the revision, inspected region, chosen encoding, pixel data, and
another cursor when one exists.
Pass the returned nextCursor as the input field cursor, retaining the
same region (for full inspection) or sinceRevision (for delta inspection).
inspect_pixels({"sinceRevision":4}) returns only the final pixels set,
replaced, or cleared after revision 4, plus a new canvas size when relevant.
The in-memory journal retains 64 mutations. After reload or journal eviction,
the tool returns history_unavailable and requests a full inspection rather
than returning an incomplete delta.
Delta pages use the same default 128 / maximum 256 pixel limit. Continue with
the same sinceRevision and returned nextCursor; changedPixelCount is the
total net change count, not the page length. Cleared pixels use
clearedRuns: [[y,xStart,xEnd],...]. If the canvas changes during pagination,
restart the delta inspection; the stale cursor is rejected. Apply a returned
canvasSize before applying the delta, including clear entries outside the
new bounds when an undo shrinks the canvas.
Encoding tags are color_groups_v1 (groups), palette_row_runs_v1
(runPalette and runs), and palette_indexed_rows_v1 (runPalette and
indexedRows). Each page has its own palette. Selection minimizes JSON
characters, not model tokens; no colors or coordinates are approximated.
Tool results omit empty warnings, empty palettes, missing occupied bounds, unlocked session state, and absent next cursors. Field names remain descriptive so smaller payloads do not make the contract harder for agents to understand.
Summary and first-page inspections return colorCount instead of the full
color-count list. Use inspect_pixels({"includePalette":true}) to get every
color and count. This option is first-page only (no cursor or sinceRevision);
pixel-page palettes needed for decoding remain present and unchanged.
The registration is top-level and imperative through
document.modelContext.registerTool. Registration and execution receive an
AbortSignal; unsupported browsers are intentionally a no-op.
Deterministic behavior is covered under test/. Tool-selection fixtures live
under evals/webmcp/fixtures/; each fixture includes the complete six-tool
page state. See evals/webmcp/README.md for what is automated versus manual.