Skip to main content
Glama

Agent Board

A local tabbed HTML viewer the Cursor agent can drive over MCP.

You keep http://127.0.0.1:4747 open in the browser. The agent opens, updates, reads, and closes tabs there instead of writing throwaway .html files into a workspace.

How it works

  • A small daemon serves the board on 127.0.0.1:4747 and remembers tabs in %LOCALAPPDATA%\agent-board.

  • Cursor talks to a stdio MCP process. That process starts the daemon if needed, then calls the local HTTP API.

  • The browser page stays connected over a WebSocket, so new pages appear as tabs immediately.

The MCP process can come and go with Cursor. The daemon stays up so the board does not reset when a chat ends.

Related MCP server: bronom

Setup on a new PC

dist/ is not in git, and Cursor only sees the MCP server and skill if they are registered on that machine. After clone:

cd C:\path\to\agent-board
npm install
npm run build

Then two copies outside the repo:

  1. MCP — add this to %USERPROFILE%\.cursor\mcp.json, with the args path pointing at this clone:

"agent-board": {
  "command": "node",
  "args": ["C:\\path\\to\\agent-board\\dist\\index.js"]
}
  1. Skill — copy .cursor\skills\agent-board to %USERPROFILE%\.cursor\skills\agent-board. A project skill only applies in this repo; the personal copy is what agents use from every other workspace. A junction stays in sync with git:

mklink /J %USERPROFILE%\.cursor\skills\agent-board C:\path\to\agent-board\.cursor\skills\agent-board

Reload MCP in Cursor after changing mcp.json. Then open http://127.0.0.1:4747 or ask the agent to present something visually.

npm start runs the daemon in the foreground. Cursor does not need this: the MCP server starts the daemon on first use. npm stop stops a running daemon.

Agent tools

Tool

Purpose

board_show

Create or replace a page (key + title + html, optional state, assets, and folder for new pages). Default focuses the tab (and opens it if it was closed). Pass background: true to update without focusing: unread blip on an open tab, or on Library if the page is closed. Returns titleKept: true when the user renamed the page in the last 24h and the new title was ignored.

board_patch

Change snippets on an existing page (id/key + edits of oldString/newString), or replace the whole HTML from a checked-out file (htmlPath). Optional expectedRevision refuses the change if the page moved. Same background/focus rules as show. Does not create a tab or reset state or wait signals.

board_screenshot

Capture a PNG (or JPEG) of a tab's page or a CSS selector. Canonical 1280×800 viewport unless you pass width/height/fullPage.

board_list

List open tabs (id, key, title, folder, …) plus closedCount. Pass query to search title, key, page text, and JSON state among open tabs.

board_library

Page the whole Library in the user's order (default 20, max 50), or search every page with query over title, key, page text, and JSON state. folder limits it to one folder and its subfolders.

board_folders

List Library folders as paths ("CLIMS/Releases") in the user's order, each with its direct page count, so an agent can file a new page in a matching folder via board_show's folder.

board_open

Open a closed page on the strip

board_read

Read a page's HTML so it can be revised (open or closed). toFile: true checks it out to a temp file for editing with file tools instead

board_get_state

Read what the user has actually typed, added, or checked off on an interactive page

board_wait

Block until the page fires a named signal (board.signal / data-board-signal), then return that signal plus the live state. Default 10 minutes. Do not poll board_get_state.

board_set_state

Write state without focusing. Unfocused open tabs and closed pages show an unread blip.

board_pin / board_unpin

Pin or unpin a tab (id or key) so Clear keeps or drops it

board_close

Close one tab, all unpinned tabs, or everything; the pages stay in the Library. Pass permanent: true to delete instead

board_template_upsert / _list / _get / _delete / _open

Reusable page templates (agent authors them only when asked; the user opens instances from the sidebar)

Reuse the same key when updating a topic. Pass a full HTML document, or a fragment (it gets a readable dark template). For a small change to an existing page, board_patch with exact oldString/newString edits instead of sending the whole document again. For a large page, board_read with toFile: true, edit the file, then board_patch with htmlPath and expectedRevision.

board_show, board_patch, and board_read also return viewUrl: the tab page on its own (http://127.0.0.2:4747/view/<id>), outside the board's iframe, so a browser tool can click, drag, and run scripts in it.

Images

board_show accepts local image files via assets (path strings, or { path, name }). The daemon copies them next to the tab and the page can reference them as asset:name:

<img src="asset:hero.png" alt="Hero">

png, jpg, gif, webp, svg, ico, and avif. 8 MB per file, 16 files / 32 MB per tab. These do not count toward the 2 MB HTML cap. Re-showing a key without assets keeps files already attached. Workspace-relative <img src> and file:// URLs do not work — tab pages are served from http://127.0.0.2 and cannot see the disk.

board_screenshot loads the tab's content page in a headless Chromium browser (Edge, Chrome, or Brave — not the board chrome) and returns an image. Pair it with board_show(..., background: true) so a design loop does not steal window focus. Default viewport is 1280×800; pass selector for one element or fullPage for a tall page.

Interactive pages

Every tab owns a JSON state object that lives in the daemon, not in the browser. The agent can read and write it whether or not the tab is focused, or the browser is even open. Pages get it as window.board, injected before any page script runs:

board.state                  // current state, readable synchronously on load
board.set({ todos })         // merge top-level keys, saved on a short debounce
board.signal("submitted")    // wake board_wait; flushes pending board.set first
board.onChange(render)       // agent or another viewer changed something
board.bind(el, "notes")      // two-way bind an input, textarea, or checkbox

A submit button can declare the same handshake without extra script: data-board-signal="submitted". The agent then calls board_wait with that signal name. board_show clears the last signal on the tab so a new wait does not instantly see the previous submit.

Interactive pages should use this instead of localStorage — all tab pages share one origin, so their localStorage collides, and the agent cannot see it.

board.bind is what makes text fields safe. It saves as you type (250 ms idle, 1 s ceiling), and when a remote change arrives for a field you are currently in, it leaves your caret and half-typed text alone, marks the field board-stale, and reconciles once you move on.

Writes merge at the top level, so the agent updating todos never disturbs the notes you are typing. To make in-progress form input completely off limits, keep it under a draft key — by convention the agent reads it but never writes it.

Conflicts

board_get_state returns a stateRevision. Passing it back as expectedRevision makes the write conditional: if the user changed the page in between, it is refused with 409 and the response carries their current state, so the agent can merge and retry. Agent writes without an expectedRevision are refused on a page that already has state, unless force is set. Writes from the page itself are never blocked — the person looking at the screen wins ties.

board_wait blocks until board.signal("name") (or data-board-signal="name") fires on that tab. It returns the signal plus the current state. Waiting for any state change would wake on every keystroke; the named signal is the handshake. After a successful wait, pass signal.revision as afterSignalRevision to wait for the next one without re-showing the page.

Data

Tabs persist in %LOCALAPPDATA%\agent-board\board.sqlite across daemon and Cursor restarts, including each tab's state object (max 256 KB per tab). Image files live in %LOCALAPPDATA%\agent-board\assets\<tabId>\. Every page lives in the **Library** until you delete it; closing a tab only takes it off the strip. **Ctrl+Z** undoes whichever is newer: the most recent close (reopens the tab), or the most recent delete. A folder delete or any other bulk delete is one undo step. Deleted pages and folders sit in the **Trash** (Library ⋯ menu → Trash) for 7 days: each row has a restore button that puts it back in the Library, and its context menu can delete it for good. After 7 days they are deleted automatically. A previous state.json is imported once and renamed to state.json.bak.

The browser Clear button closes unpinned tabs. Pinned tabs stay until you close or delete them. Shift+click a tab's × deletes the page (the notice has Undo). Ctrl+S downloads the current page as HTML (markup only). Settings and the tab/Library context menus export a .board.json pack that includes state, images, Library folder and position, and the templates behind any template pages (Export all includes every template; a folder's menu exports just that folder); Import (or a drop on Settings, the tab strip, or the Library) restores those files.

The sidebar has Library and Templates. The Library is a tree of folders and pages in your own order: drag rows to reorder or file them, drag a page onto the strip to open it there, or drag a tab into the Library to file it and close it. Open pages have an accent bar, the current tab's row is filled, and pinned pages get a faint warm tint. Hovering a tab or Library row shows its full title, id, created and updated times, and folder. Templates are reusable pages with a form. The agent creates a template when you ask; you open copies from the list. A few built-ins (Embed, Markdown note, Todo list) sit in a collapsible Built-in group under your own; opening one first adds a copy to your templates and the page uses that copy, so app updates never change your pages. Right-click a built-in to add it without opening a page. Updating a template refreshes every page created from it. A linked page's HTML cannot be edited — only the template can. If a template change breaks that page's data, the board blocks the page until the agent fixes the data.

Embedding a site

A page whose <head> has <meta name="agent-board-embed" content="URL"> is shown by pointing the tab iframe straight at that URL, instead of nesting it inside the tab page. The Embed template does this. Only http: and https: URLs outside the board's own origin count; anything else falls back to rendering the page's HTML. The stored HTML stays a small wrapper, so board_read and search see the URL but not the site's content.

A direct frame keeps the site on the same site as the board chrome (127.0.0.1), so logins that use SameSite=Lax cookies (ComfyUI-Login, for example) keep working. Use 127.0.0.1, not localhost: the browser treats them as different sites. Board shortcuts (Ctrl+S, Ctrl+D, …) don't reach the board while focus is inside the embedded site.

Hiding a tab from the agent

Right-click a tab or Library row and choose Hide from agent, or tick Hide from agent in a template's Open/Edit form. Hidden tabs show an eye icon. To the agent they don't exist: they're left out of board_list, board_library, search, activeId, bulk board_close, and template instance counts, and every per-tab tool returns "tab not found". A board_wait already running on the tab ends as if the tab had closed. board_show with a hidden tab's key creates a separate tab instead of overwriting it. Only the board UI can change the flag; the MCP marks its requests with an x-agent-board-client: agent header and cannot flip it.

This is a guardrail on the board's tools, not a sandbox. An agent with a shell or browser could still call the HTTP API without the header, or open the embedded URL itself.

Port: 4747 (override with AGENT_BOARD_PORT). Bound to localhost only.

Tab pages load in an iframe from http://127.0.0.2:4747 so they can use localStorage without accessing the board chrome or API. Refresh the board after upgrading so the new iframe sandbox takes effect.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables MCP clients to control a real local browser window for web automation tasks such as clicking, typing, scrolling, and taking screenshots.
    7 npm
    -
  • A
    license
    Not graded
    quality
    C
    maintenance
    Exposes a persistent, visible multi-tab Electron browser to AI clients through MCP, enabling tab management, navigation, interaction, screenshots, and JavaScript evaluation.
    1
    MIT
  • F
    license
    Not graded
    quality
    A
    maintenance
    Enables AI agents to control a persistent local browser with live tabs, navigation, interaction, inspection, and state management through MCP.
    6
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables local control of existing Chrome/Chromium browser tabs through MCP, including tab management, navigation, content reading, screenshots, and page interaction.
    Apache 2.0