earmark

earmark

MCP server that lets coding agents receive UI feedback from a browser overlay—clicked elements, selectors, source paths, and computed styles—and manage it with watch, ask, acknowledge, resolve, and dismiss tools.

Category
访问服务器

README

earmark

Click an element in your running app, say what should change, and your coding agent gets the CSS selector, the source file and line, the component path, the computed styles and the box geometry — instead of "the button on the right looks wrong".

Works in any framework. No build step required for the overlay.

┌─ browser ──────────────┐        ┌─ broker ────────┐        ┌─ agent ─────────┐
│ click → annotate       │ POST   │ store + SSE     │  MCP   │ list / watch    │
│ pins, panel, markdown  │───────▶│ long-poll       │◀──────▶│ ask / resolve   │
│                        │◀───────│ .earmark/*.json │        │ dismiss         │
└────────────────────────┘  SSE   └─────────────────┘        └─────────────────┘

Try it in 30 seconds

npm install && npm run example

Open http://127.0.0.1:5173/examples/vanilla/, click the arrow in the toolbar (bottom right) or press alt+a, then click anything on the page.

The landing page and the full documentation are served alongside it at http://127.0.0.1:5173/site/ and http://127.0.0.1:5173/site/docs.html. Both are live at https://nahar-strativ.github.io/earmark/ and each is a single self-contained file with no dependencies.

For live agent sync, run the broker in a second terminal:

npm run server

Install

npm install -D earmark
import { createEarmark } from 'earmark';

if (import.meta.env.DEV) {
  createEarmark();
}

No bundler:

<script type="module" src="/node_modules/earmark/src/index.js" data-earmark-auto></script>

Options

createEarmark({
  endpoint: 'http://127.0.0.1:7331', // or false for copy-paste only
  hotkey: 'alt+a',
  theme: 'auto',                     // 'auto' | 'light' | 'dark'
  persist: true,                     // keep annotations across reloads
  onAnnotate: (annotation) => {},
});

The endpoint defaults to the local broker and degrades silently when nothing is listening — the overlay still works, the sync dot just goes grey.

TypeScript

Every package ships hand-written declarations — there is no build step here, and an emitted .d.ts would need one. The domain types (Annotation, Target, Session, Status, Priority) live in earmark and are re-exported by earmark-server and earmark-mcp, so an annotation is the same type on both sides of the wire.

import { createEarmark, type Annotation } from 'earmark';

const overlay = createEarmark({ theme: 'dark' });
const pending: Annotation[] = overlay.annotations;

Using it

Tool What it does
Click an element. Shift-click to add more, then click to finish.
T Select text — the exact string is the most greppable thing you can hand an agent.
Drag a region. Reports every element inside, or flags an empty area.
Freeze everything moving — CSS animations, element.animate(), <video>, <audio>.
Panel: review, delete, answer the agent, copy markdown.

⌘↵ saves an annotation, esc cancels, alt+a toggles picking. Each annotation can be marked high, normal or low priority; high sorts first for the agent.


Copy-paste mode

Click Copy markdown in the panel and paste into your agent:

## UI feedback — 1 annotation

- **Page:** http://localhost:5173/dashboard
- **Viewport:** 1440×900 @2x, dark mode
- **Framework:** react

### 1. Export button padding is too tight — needs 10px 16px

- **Element:** `<button>` <ExportButton>
- **Selector:** `[data-testid="export-btn"]`
- **Source:** `src/components/Card.tsx:42:7`
- **Component path:** App › Dashboard › Card › ExportButton
- **Text:** "Export"
- **Box:** 66×37 at (194, 376)
- **Computed:** padding: 7px 13px; border-radius: 8px; font-size: 13px
- **Ancestors:** div.row ← section.card ← main

Agent sync mode (MCP)

claude mcp add earmark -- npx -y earmark-mcp

Or, to write it into the project's .mcp.json:

npx earmark-mcp init

That one process runs the MCP server and the broker the browser talks to. When something is not working, ask it why:

npx earmark-mcp doctor
✓ Node version: v24.12.0
✓ sqlite backend: available
✓ MCP registration: earmark is registered in .mcp.json
✓ Broker: responding on http://127.0.0.1:7331 — 1 annotations, 2 sessions
✓ Browser overlay: http://localhost:5173/ (1 annotations)

Each failing check prints the command that fixes it, and doctor exits non-zero so CI can use it.

Tools

Tool Purpose
earmark_list_annotations Outstanding work, as markdown (or format: "json"); scope with session
earmark_watch_annotations Blocks until the human annotates something
earmark_get_annotation One annotation with its full reply thread
earmark_list_sessions Which browser tabs are open, and which routes were annotated
earmark_get_session One tab with every annotation it produced
earmark_acknowledge "I've read it, I'm on it" — the pin turns blue
earmark_ask Ask a clarifying question — the pin turns amber
earmark_resolve Mark done with a summary — the pin turns green
earmark_dismiss Decline with a reason the human sees
earmark_clear Delete everything
earmark_status Is the overlay connected? What endpoint should it use?

The fix loop this enables:

watch → acknowledge → read the source path → edit the file → resolve → watch

acknowledge matters on anything slow: without it, an agent halfway through a refactor looks exactly like an agent that ignored you. Blue pin means picked up, green means actually done.

When the feedback is ambiguous, ask instead of guessing. The question appears on the pin; the human's answer wakes the next watch.

Statuses

openacknowledgedresolved, with needs-input when the agent is waiting on a human and dismissed when it declines. Pins are colour-coded: orange, blue, green, amber, grey.

Sessions

A session is one browser tab, not one page load — the id lives in sessionStorage, so it survives reloads. Annotations carry their own page.url, so a session that wandered across three routes gives an agent one group with three differently-routed items.

SPA navigation is tracked too: pushState, replaceState, popstate and hashchange all update the session's route list. A tab counts as connected for exactly as long as its SSE stream is open.

curl http://127.0.0.1:7331/sessions

Source file paths

Selectors tell an agent what to grep for. Source paths tell it exactly where to look, which is the difference between one edit and three greps.

React 19 removed the runtime _debugSource fiber field, so this is done at build time:

// vite.config.js
import earmark from 'vite-plugin-earmark';

export default {
  plugins: [react(), earmark()],
};

Every intrinsic JSX element gets data-earmark-src="src/Card.tsx:42:7" during vite dev. The plugin also injects the overlay, so createEarmark() in your app code becomes optional.

earmark({
  inject: false,        // do not auto-mount the overlay
  endpoint: '…',        // passed through to createEarmark
  applyInBuild: true,   // also stamp production builds (off by default)
})

Without the plugin everything still works — you get selectors, component names and text, just not file:line. You can also add data-earmark-src by hand.

Next.js

Next compiles with SWC, so a Babel plugin would silently switch the whole project off SWC and slow every build down. earmark-loader is a pre-loader instead — it sees the source you wrote and leaves the rest of the pipeline alone. It covers both webpack and Turbopack:

// next.config.mjs
import { withEarmark } from 'earmark-loader/next';

export default withEarmark({
  // your config
});
withEarmark(config, {
  applyInBuild: true,   // stamp production builds too (off by default)
  root: process.cwd(),  // project root for the reported paths
  exclude: 'legacy/',   // regexp source string
})

Both compilations are stamped, client and server. That is deliberate: Next renders your components on the server, and React hydration will not add an attribute the server HTML did not have — stamping one side only means the attribute goes missing until something re-renders, with a hydration mismatch on the way.

The wrapper does not mount the overlay for you. Next has no index.html to inject into, so add it once in a client component:

'use client';
import { useEffect } from 'react';

export function Earmark() {
  useEffect(() => {
    if (process.env.NODE_ENV !== 'development') return;
    import('earmark').then(({ createEarmark }) => createEarmark());
  }, []);
  return null;
}

Svelte

Vite users — including SvelteKit — get .svelte stamping from vite-plugin-earmark with no extra configuration. Every plain element in the markup is stamped before the Svelte compiler sees it:

<button data-earmark-src="src/lib/Card.svelte:12:3" class="go">Go</button>

Components, <svelte:*>, <slot>, comments, {expressions} and the contents of <script>/<style> are left alone — a stamp in the wrong place there would not produce a wrong line number, it would produce a file that does not compile.

For a Svelte build that is not Vite, use the preprocessor:

// svelte.config.js
import { earmarkPreprocess } from 'earmark-stamp';

export default { preprocess: [earmarkPreprocess()] };

Svelte still has no runtime hook for component names, so stamping is the whole story there — but CSS rule mapping (below) works regardless.

Plain HTML and CSS — no build step

A static site has no build to stamp, so earmark resolves the source at annotation time instead:

  • HTML — the document is re-fetched and parsed with position tracking, then the element's child-index path is walked in the source. Every step is checked against the live tag name, so a framework-rendered page (where the served HTML is just a shell) reports nothing rather than inventing a line.
  • CSS — every rule that matches the element, mapped back to the file and line that declares it. This one works everywhere, framework or not.
- **Source:** `index.html:101:11` _(resolved from the served HTML)_
- **CSS rules that style it:**
  - `button``index.html (inline <style>):49`
    - padding: 7px 13px; border-radius: 8px; border: 1px solid var(--line);
  - `button.primary``index.html (inline <style>):59`
    - background: var(--accent); color: rgb(255, 255, 255);

The agent now knows the padding it has to change lives at line 49 in the generic button rule, not in .primary. Inline <style> blocks are offset into their host document; external stylesheets report their own path; cross-origin stylesheets are skipped because their contents are unreadable.


Standalone broker

npx earmark-server --port 7331
curl http://127.0.0.1:7331/markdown
Route
GET /health liveness + counts
GET /annotations?status=open&session=ID list
POST /annotations create (batch)
GET /annotations/wait?since=N&timeout=30000 long-poll
PATCH /annotations/:id update status
POST /annotations/:id/replies append to the thread
DELETE /annotations/:id · DELETE /annotations remove · clear
POST /session register a tab / record a route change
GET /sessions · GET /sessions/:id tabs, with counts and annotations
GET /events?session=ID SSE stream; also the tab's liveness signal
GET /markdown the agent-facing document

Flags: --host --store --file --no-persist --webhook --token --quiet.

Storage

--store json (default) writes a readable .earmark/annotations.json on a 250 ms debounce. --store sqlite writes each change immediately to .earmark/annotations.db through node:sqlite, so a crash loses at most the statement in flight — no dependency, Node 22.5+, and it falls back to JSON if unavailable. --store memory keeps nothing.

Webhooks

npx earmark-server --webhook https://hooks.example/earmark

Also EARMARK_WEBHOOK_URL and EARMARK_WEBHOOKS (comma-separated). Every annotation event is POSTed with an x-earmark-event header. Delivery is fire-and-forget with a 5 s timeout and one retry, so a dead endpoint cannot stall the annotation loop.


Security

This is a development tool.

  • The broker binds 127.0.0.1 only. Do not bind it to 0.0.0.0.
  • CORS is open by design — your dev server is on an arbitrary origin.
  • Any page open in your browser can reach a loopback port. Pass --token SECRET if that matters on your machine.
  • Webhooks send annotation content off your machine — page URLs, element text, and whatever you typed. Only configure endpoints you control.
  • Source resolution re-fetches your own page and stylesheets from the same origin. Nothing is sent anywhere.
  • Do not run it on a shared or public host.

Tests

npm test

Eleven suites, 122 tests: store and HTTP behaviour, the MCP surface driven by a real stdio client, the overlay's sync client, both persistence backends, webhook delivery, the init/doctor CLI, the source resolvers, JSX and Svelte stamping, the webpack/Turbopack loader, and a type-level check of the published declarations.

npm run types

Type-checks the hand-written declarations. To check the assembled product rather than its units:

npm run verify

Twenty features against real servers, a real MCP process over stdio, real files and a real webhook listener: every broker route, all three storage backends across a restart, the eleven MCP tools, stamping through all three integrations, and both CLI commands. It exists because a bug once passed 122 unit tests and failed the moment the pieces were put together.

Type-checks test/types/check.ts against the hand-written .d.ts files. The @ts-expect-error lines in it are assertions too — they fail the build if the error they name stops happening.


Not supported

No screenshots, by decision rather than omission. Your agent already drives a browser; earmark hands it a verified selector and a URL, so it can capture the element itself at full fidelity. Shipping html2canvas would send a re-render of your page rather than what the browser painted, which is the wrong answer for a tool whose promise is "here is what I am looking at".

Closed shadow roots are opaque to every script, so a component using one is annotated as the component; open roots are picked into and reported with the expression that reaches the element. Nothing inside a shadow root or a canvas has a source line, because that markup is script-created.

Cross-origin iframes stay invisible, because that is a browser security boundary rather than a gap to work around; same-origin frames are pickable, and their own HTML and CSS are resolved. A canvas has no DOM, so earmark reports its coordinate space rather than pretending otherwise. Touch works, but this is still a tool for the machine you develop on. See plan.md for the full open list and the reasoning behind every design decision.

Releasing

All six packages are on npm. To cut the next version, bump the versions (keeping the cross-package pins in step, or a fresh install pulls the older ones underneath) and then:

npm run release

That runs the suite and the live verification first, then npm publish --workspaces, which publishes in dependency order. It needs npm login, and publishing cannot be undone, so it is a command you run rather than one that runs for you.

npm pack --dry-run inside any package shows exactly what would ship. It proves the file list but not the package: an exports map can still block a subpath that tools expect, which is only visible once something installs and resolves it.


License

MIT. Clean-room implementation — not derived from any other tool's source.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选