Skip to main content
Glama
README.md
# Annotation Overlay MCP

DOM-aware visual annotation overlay for web feedback. Draw arrows, boxes, text, freehand strokes, click elements, and annotate text selections — all in one toolbar. Submit structured annotations to Claude Code via MCP.

**6 interaction modes. Zero JS dependencies. Chrome Extension + MCP Server.**

```
[Browser Page]  ←→  [Chrome Extension]  ←→  [MCP Server]  ←→  [Claude Code]
   overlay.js         bridge.js              HTTP + stdio       read/clear tools
```

## Quick Start

### 1. Install & Start MCP Server

```bash
git clone https://github.com/cc10143/annotation-overlay-mcp.git
cd annotation-overlay-mcp
npm install
npm start
# → HTTP server on port 3847 + MCP stdio transport connected
```

### 2. Load Chrome Extension

1. Open `chrome://extensions`
2. Enable **Developer mode**
3. Click **Load unpacked**
4. Select the `extension/` directory

The overlay auto-injects on every page, but the toolbar stays **hidden during normal browsing** (it's a tool for agents, not a resident UI). To annotate, either press `Ctrl+Shift+A`, or — in the feedback flow — have the agent call `set_annotation_mode(true)`, which shows the toolbar on the active tab.

### 3. Configure Claude Code MCP

```json
// ~/.claude/settings.json
{
  "mcpServers": {
    "annotation-overlay-mcp": {
      "command": "node",
      "args": ["D:/KaiFa/annotation-overlay/server/index.js"]
    }
  }
}
```

Or use the CLI:

```bash
claude mcp add annotation-overlay-mcp -- node D:/KaiFa/annotation-overlay/server/index.js
```

## Usage

### Annotation Workflow

1. **Agent launches the annotation browser (primary)** — Claude Code runs the `annotation-browser-launch` skill: Playwright opens chromium with the extension + a persistent profile, navigates to the target page, and shows the toolbar. (Fallback: the agent calls `set_annotation_mode(true)` on the user's own Chrome.)
2. **Annotate** — use any of the 6 tools to mark issues
3. **Submit** — click Submit to send structured JSON to the MCP server. The extension automatically captures a **viewport screenshot with annotations overlaid** and uploads it; the server saves it to `~/.annotation-overlay/screenshots/`. Submit also hides the toolbar and deactivates the overlay.
4. **Agent reads** — Claude Code calls `read_annotations` → gets annotations + `screenshotPath` → feeds the screenshot to a vision-capable tool → processes feedback
5. **Fix & verify** — after the fix, the agent refreshes the annotation browser (via CDP reconnect), then compares a fresh screenshot against the annotated `screenshotPath` to confirm the fix actually landed
6. **Next round** — the agent re-activates the overlay and repeats

### Tools

| Tool | Label | How | DOM Link |
|------|-------|-----|----------|
| Arrow | ➤ | Click & drag → arrow | Element under arrowhead |
| Box | □ | Click & drag → rectangle | **Region container** + element at box center |
| Text | T | Click → type text label | Element under click point |
| Freehand | ✎ | Click & drag → freeform drawing | **Region container** + bbox center element |
| **Select** | **+** | Hover highlights blue → click to pin numbered badge | Clicked element |
| **TextSel** | **[ ]** | Select page text → annotate instantly | Containing element |

Drawing annotates **immediately** — no comment prompt interrupts your flow. To add a comment to an existing annotation, switch to the **Select** tool and **double-click** the drawing (or the numbered badge). Only the Text tool still prompts, because its text is the annotation itself.

### Keyboard Shortcuts

| Key | Action |
|-----|--------|
| `Ctrl+Shift+A` | Toggle overlay |
| `1–6` | Switch tool |
| `Ctrl+Z` | Undo last annotation |
| `Ctrl+Shift+Z` / `Ctrl+Y` | Redo |
| `Double-click` | Edit annotation comment (select tool) |
| `?` | Show shortcut panel |
| `Escape` | Cancel drawing / close overlay |
| `Enter` | Confirm comment |
| `Shift+Enter` | Newline in comment |

### Colors

5 preset colors: red `#e94560`, blue `#4080f0`, green `#2ecc71`, yellow `#f1c40f`, purple `#9b59b6`.

### Screenshot

On Submit, the extension captures the current viewport **with annotations overlaid** — the canvas strokes and numbered badges are DOM, so they appear in the shot naturally; only the toolbar is hidden during capture. The screenshot is uploaded to the MCP server and saved to `~/.annotation-overlay/screenshots/anno-<timestamp>.png`.

`read_annotations` returns the absolute `screenshotPath` in its response. Pass it to a vision-capable tool (e.g. a vision MCP) to see exactly what the user is pointing at — this is how a non-multimodal agent gets visual context for the annotations.

For **before/after verification**, the agent calls `capture_page` after the fix reloads the page. The extension captures the current viewport (clean — the overlay is inactive until toggled) and the server exposes it as `afterScreenshotPath` in `read_annotations`. Feeding both `screenshotPath` (annotated "before") and `afterScreenshotPath` (clean "after") to a vision-capable tool lets the agent confirm a fix landed instead of working blind.

_Extension only._ Without the extension bridge (standalone script injection), submit proceeds without a screenshot.

## MCP Tools

### `read_annotations`

Read all pending annotations. The response also includes `screenshotPath` — the absolute path to the last annotated viewport screenshot (saved on Submit) — and `afterScreenshotPath` — the absolute path to the last `capture_page` screenshot (clean post-fix viewport), both for vision-capable consumption.

Each annotation includes:

```json
{
  "id": "uuid",
  "type": "circle",                       // arrow | circle | text | freehand | select | textsel
  "comment": "user feedback text",        // empty until double-clicked in select mode
  "selector": "div.card:nth-child(1) > button.btn-primary",
  "fallbackSelectors": [
    { "type": "id", "value": "#submit-btn" },
    { "type": "cssPath", "value": "div.card:nth-child(1) > ..." },
    { "type": "contentHash", "value": "Buy Now-a3f8b2c1" }
  ],
  "tagName": "BUTTON",
  "classes": ["btn-primary"],
  "elementText": "Buy Now",
  "contentHash": "Buy Now-a3f8b2c1",
  "region": {                             // box/freehand only: the container element
    "selector": "#card",                  //   covering the drawn area (deepest ancestor
    "fallbackSelectors": [                //   whose rect covers ≥60% of the drawing)
      { "type": "id", "value": "#card" },
      { "type": "cssPath", "value": "div.card" },
      { "type": "contentHash", "value": "Card A-..." }
    ],
    "tagName": "div",
    "classes": ["card"]
  },
  "position": { "x": 100, "y": 200, "w": 200, "h": 100 },
  "color": "#e94560"
}
```

**`screenshotPath`** — added to the response when a screenshot was captured on submit, e.g. `"C:\Users\you\.annotation-overlay\screenshots\anno-2026-08-12T06-27-48.png"`. Pass it to a vision-capable tool to see the page with annotations overlaid.

**`afterScreenshotPath`** — added to the response after an agent-requested `capture_page`, e.g. `"C:\Users\you\.annotation-overlay\screenshots\anno-2026-08-12T06-30-12.png"`. This is the clean post-fix viewport; compare it against `screenshotPath` to verify a fix landed. **`afterScreenshotTabUrl`** is the URL of the tab that was captured — confirm it matches the page you expected (the capture is of the active tab). All three are cleared by `clear_annotations`.

**`httpOwnedExternally`** — `true` when another annotation-overlay instance holds the HTTP port (e.g. a stale `npm start`, or an orphaned MCP child from an abrupt Claude Code shutdown), with `externalHttpVersion` naming that instance. `read_annotations`/`clear_annotations` still work against the shared store, but `capture_page`/`set_annotation_mode` may fail until the stale process is killed (`netstat -ano | grep :3847` → `taskkill //PID <pid> //F`). The server no longer crashes on a port conflict — it keeps the MCP tools alive and reports this flag instead.

**`position` shape varies by type** (viewport coordinates):

| Type | position |
|------|----------|
| arrow | `{ "start": {"x","y"}, "end": {"x","y"} }` |
| circle / select / textsel | `{ "x", "y", "w", "h" }` |
| text | `{ "x", "y" }` |
| freehand | `{ "points": [{"x","y"}, ...] }` |

**`region`** (box/freehand only) is the container element that best represents the drawn area — e.g. the card `<div>` the user circled — with the same fallback chain as `selector`. Use it to locate the region when the box center happens to sit on a child element.

### `capture_page`

Capture the current page viewport and save it as the **"after"** state for before/after verification. The server asks the extension (via its existing ~10s badge poll) to capture the active tab of the focused window; the shot is clean — the extension hides any annotation overlay chrome before capturing. Returns the absolute path to the saved PNG plus the URL of the captured tab, e.g.:

```json
{ "ok": true, "path": "C:\\Users\\you\\.annotation-overlay\\screenshots\\anno-2026-08-12T06-30-12.png", "tabUrl": "https://example.com/" }
```

**Verify `tabUrl` matches the page you expect** — `capture_page` captures the active tab, so if the user switched tabs it would show the wrong page (the server can't tell). May take up to ~15s (extension poll + capture). Requires the extension to be loaded. Pair the result with `screenshotPath` from `read_annotations` to confirm a fix landed.

### `set_annotation_mode`

Show or hide the annotation toolbar on the active tab, so the user can give visual feedback. The overlay is injected on every page but the toolbar is **hidden by default** — this is how the agent turns it on before asking the user to annotate, and off after processing:

```json
{ "ok": true, "enabled": true, "tabUrl": "https://example.com/" }
```

The extension relays the change to the page's overlay via its ~10s badge poll. May take up to ~15s. Requires the extension to be loaded and the target page to be the active tab. The user can also toggle manually with `Ctrl+Shift+A`.

### `clear_annotations`

Clear all stored annotations. Call after processing feedback.

## Claude Code Skills

The repo ships two skills (in `skills/`) that encode the annotation feedback loop. Install them to `~/.claude/skills/` with:

```bash
npm run install-skills
```

| Skill | Purpose |
|-------|---------|
| `annotation-browser-launch` | **Primary flow.** The agent launches its own annotation browser (Playwright chromium + extension + persistent profile + fixed CDP port), navigates the target page, and shows the toolbar. The agent natively controls navigation/refresh/screenshot afterward. |
| `annotation-feedback-wait` | Wait for the user's submission (background watcher polling the annotation count — the MCP server can't push), then `read_annotations`, process, and `clear_annotations`. |

Edit the skills in `skills/` (the repo is the source of truth), then re-run `npm run install-skills` to sync.

## Selector Fallback Chain

When the agent regenerates the page, CSS selectors may break. Each annotation carries a fallback chain:

1. **id** — `#element-id` (most stable)
2. **cssPath** — `div.card:nth-child(1) > button.btn-primary`
3. **contentHash** — `Buy Now-a3f8b2c1` (first 40 chars of text + djb2 hash)

The agent should try each fallback in order when resolving elements after page changes.

## Standalone Use (Without Extension)

The overlay can be injected into any page via script tag or browser console:

```html
<script src="annotation-overlay.js"></script>
```

Or via Tandem evaluate / Playwright:

```js
// Tandem
tandem_devtools_evaluate({ function: "..." }) // paste annotation-overlay.js contents

// Playwright
await page.evaluate(fs.readFileSync('annotation-overlay.js', 'utf-8'));
```

Public API:

```js
__annotationOverlay.activate()    // show toolbar
__annotationOverlay.deactivate()  // hide overlay
__annotationOverlay.serialize()   // → JSON string
__annotationOverlay.clear()       // remove all annotations
__annotationOverlay.submit()      // send to MCP server via direct fetch
```

## Architecture

```
┌──────────────────────────────────────────────────────┐
│  annotation-overlay.js  (src/, ~1600 lines)          │
│                                                      │
│  ┌──────────┐  ┌──────────┐  ┌──────────────────┐   │
│  │ Toolbar  │  │  Canvas  │  │  DOM Bridge      │   │
│  │  6 tools │  │  2D ctx  │  │  elementFromPoint │   │
│  │  5 colors│  │  DPR     │  │  CSS selector gen │   │
│  │  Submit  │  │  undo    │  │  contentHash      │   │
│  └────┬─────┘  └────┬─────┘  └────────┬─────────┘   │
│       │             │                │               │
│       └─────────────┴───────┬────────┘               │
│                             │                         │
│                    ┌────────▼────────┐                │
│                    │  Annotation Model│               │
│                    │  + Serializer   │               │
│                    │  + Fallback     │               │
│                    └────────┬────────┘               │
└─────────────────────────────┼────────────────────────┘
                              │ postMessage
┌─────────────────────────────┼────────────────────────┐
│  Chrome Extension           ▼                         │
│  ┌──────────────┐  ┌──────────────┐                  │
│  │  bridge.js   │  │  service-    │                  │
│  │  (ISOLATED)  │  │  worker.js   │                  │
│  │  inject +    │  │  badge +     │                  │
│  │  relay       │  │  relay       │                  │
│  └──────┬───────┘  └──────┬───────┘                  │
└─────────┼──────────────────┼──────────────────────────┘
          │                  │ HTTP (port 3847)
┌─────────▼──────────────────▼──────────────────────────┐
│  MCP Server (Node.js, single process)                  │
│  ┌──────────────┐  ┌──────────────┐                   │
│  │  Express API │  │  MCP stdio   │                   │
│  │  POST/GET/   │  │  read_       │                   │
│  │  DELETE ann  │  │  clear tools │                   │
│  └──────┬───────┘  └──────┬───────┘                   │
│         └────────┬─────────┘                           │
│         ┌────────▼────────┐                            │
│         │  File Store      │                            │
│         └─────────────────┘                            │
└────────────────────────────────────────────────────────┘
```

> On Submit the extension captures the annotated viewport; the server saves it to `~/.annotation-overlay/screenshots/` and `read_annotations` surfaces the path via `screenshotPath`.

## Comparison

| | Annotation Overlay MCP | Vibe Annotations | Dongke-X/redline |
|---|---|---|---|
| Drawing tools | 4 (arrow/box/text/freehand) | None | Full HTML editor |
| Click-to-select | Yes (+ badge) | Yes | Full edit |
| Text selection annotation | Yes | No | No |
| MCP automation | Yes (stdio) | Yes (SSE/HTTP) | No (file-based) |
| Selector fallback | id→cssPath→contentHash | source maps | id→cssPath→contentHash |
| License | MIT | PolyForm Shield | Apache 2.0 |
| Dependencies | 3 (Express + cors + MCP SDK) | Many (WXT, etc.) | Many (React, etc.) |

## Configuration

| Env Var | Default | Description |
|---------|---------|-------------|
| `ANNO_PORT` | `3847` | HTTP server port |

## License

MIT — Copyright (c) 2026 gaogao

TDQS

A4/5.0

Scored across 2 tools

Disambiguation5/5

read_annotations and clear_annotations have completely distinct purposes: one retrieves feedback, the other removes it. There is no overlap or ambiguity between the two operations.

Naming Consistency5/5

Both tools follow the same verb_noun pattern (read_annotations, clear_annotations), with clear and predictable naming. The convention is consistent across the entire server.

Tool Count3/5

With only 2 tools, the server feels minimal. While the read/clear pair covers the core workflow, the count is borderline for a standalone MCP server.

Completeness4/5

The server covers the essential operations for consuming and resetting annotation feedback. There is no per-annotation update or delete, but the batch read/clear model fits the described usage pattern well.

Maintenance

ActivityMaintained
ResponsivenessUnresponsive