Skip to main content
Glama
README.md
# localmcp

**A local, read-optimized browser for AI agents.** Your agent reads any page — including the ones you're signed in to — as focused, token-budgeted markdown. Runs a real Chromium on your machine. No cloud relay, no API key, no account.

```bash
claude mcp add localmcp -- npx -y @network101/localmcp   # Claude Code
npx @network101/localmcp init                            # everything else: prints config for your clients
```

[![npm](https://img.shields.io/npm/v/localmcp)](https://www.npmjs.com/package/localmcp) · MIT · Node ≥ 20 · [Website](https://localmcp.pages.dev)

---

## Why

| Option | Problem for agents |
|---|---|
| Built-in web fetch (e.g. Claude Code's WebFetch) | No cookies, no JavaScript, refuses localhost, and large pages come back as a small-model summary rather than the page. |
| Cloud scrapers (Firecrawl, Jina, Browserbase) | Your Stripe dashboard, internal wiki, and `localhost:3000` go through someone else's servers — or can't be reached at all. |
| Full automation MCPs (e.g. Playwright MCP) | Great for driving a browser. For *reading*, one page snapshot is 13k–87k tokens of accessibility tree ([measured](benchmarks/results.md)). |
| CLI browser tools | Need a shell. Claude Desktop and other sandboxed clients don't have one. |

localmcp is the reading-first option: the page itself — no summarizer in the middle — focused and under a token budget, read by a browser on your machine. Five MCP tools, or three shell commands. See the [benchmark](benchmarks/results.md).

## What's new in 0.2

- **Real authenticated browsing.** `npx @network101/localmcp login <url>` opens a visible browser on a private profile. Sign in once; the agent reuses the session. Or attach to your own Chrome over CDP.
- **Focus + budget.** `browse({ url, focus: "rate limits", maxTokens: 1500 })` ranks sections by relevance and returns only what fits — and tells the agent which sections it left out.
- **Structured extraction with MCP sampling.** `extract` returns JSON-LD, meta tags, and tables as row objects. Pass a `schema` and your *client's own model* fills it — no extra API key.
- **Current MCP spec.** Tool `annotations`, `outputSchema` + `structuredContent`, server `instructions`, progress notifications, and diff snapshots as resources.
- **Safety by default.** Domain allow/deny policy, read-only mode, dangerous schemes blocked, and every page wrapped in an untrusted-content fence against prompt injection.
- **Metadata-first reading (0.2.1).** If a site publishes markdown for agents (a `text/markdown` alternate link, or `Accept: text/markdown`), localmcp reads that instead of the rendered HTML. Stripe's API reference drops from 428k tokens of HTML to the publisher's own markdown. Every result opens with a page card (type, site, author, published/updated dates, canonical URL, source) read from JSON-LD, OpenGraph, `<meta>` and front matter, and `links` points out a site's `/llms.txt`.

See [CHANGELOG.md](CHANGELOG.md) for the full list and migration notes.

## How a page becomes agent-readable

1. **Ask the publisher.** Use a declared `<link rel="alternate" type="text/markdown">` on the same origin, or request the page with `Accept: text/markdown`. Many docs platforms (Stripe, Vercel, Cloudflare, Anthropic, GitHub) serve clean markdown this way. It's used only from the page's own origin and only if it matches the rendered page's title; otherwise the rendered page wins and the result says so.
2. **Read the metadata.** Use JSON-LD (including `@graph`), OpenGraph, `<meta>`, the canonical link and markdown front matter to build a one-line page card covering what the page is, who wrote it, how fresh it is, and where the text came from.
3. **Read the structure.** If there's no publisher markdown, render the page, strip chrome (nav, footer, banners, hidden and `aria-hidden` nodes), keep semantic landmarks, headings and tables, and convert to GFM.
4. **Rank and budget.** `focus` ranks sections; `maxTokens` caps the result and lists omitted headings.
5. **Fence it.** Wrap the result in `<untrusted-page-content>` with typed `structuredContent` (`source`, `card`).

Example card:

```
> TechArticle · Vercel · updated 2026-09-13 · source: publisher markdown (declared text/markdown alternate)
```

Turn publisher markdown off with `"distill": { "publisherMarkdown": false }`. Cross-origin alternates are always ignored, so a page can't point the agent at another host.

## Tools

| Tool | What it does | Annotations |
|---|---|---|
| `browse` | Page → clean markdown. `focus`, `maxTokens`, `diff`, `elements`, `waitFor`. | read-only |
| `extract` | JSON-LD, meta/OpenGraph, tables as rows, headings; optional `schema` filled via sampling. | read-only |
| `links` | De-duplicated absolute links; filter by `sameOrigin` or `match`. | read-only |
| `screenshot` | Viewport, full page, or one `selector`; `png` or `jpeg`. | read-only |
| `interact` | `click` `fill` `select` `press` `check` `uncheck` `hover` `scroll` `wait`, then returns the resulting page. | **destructive** |

Read-only annotations let clients auto-approve reads while still confirming `interact`.

### From a shell

The same tools and policy, with no tool schema in your agent's context:

```bash
npx @network101/localmcp read https://docs.stripe.com/api --focus "pagination" --max-tokens 1500
npx @network101/localmcp extract https://example.com/pricing --schema '{"plans":[{"name":"string"}]}' --json
npx @network101/localmcp links https://docs.example.com --same-origin --match /api/
```

`--json` prints `structuredContent`; otherwise you get the same fenced markdown the MCP tools return. Exit code 1 means a policy block or a page error.

### browse

```js
browse({ url: "https://docs.stripe.com/api", focus: "pagination", maxTokens: 1500 })
```

```
# Pagination | Stripe API Reference
https://docs.stripe.com/api/pagination · 1,204 tokens · 98% smaller than raw HTML · truncated to budget
> Focus: pagination

<untrusted-page-content source="https://docs.stripe.com/api/pagination">
…the relevant sections, in page order…
</untrusted-page-content>

_Omitted sections — call again with `focus` or a larger `maxTokens` to read them: Errors · Idempotent requests · …_
```

- **`diff: true`** — first call saves a baseline; later calls return only a unified diff of what changed (the old `watch` tool; `watch` still works as an alias).
- **`elements: true`** — appends visible buttons/links/inputs with CSS selectors verified unique in the live DOM, ready for `interact`.
- **`waitFor: "#app table"`** — wait for a late-rendering SPA element before reading.

### extract

```js
extract({ url: "https://example.com/pricing", schema: { plans: [{ name: "string", price: "string" }] } })
```

`structuredContent` always contains `title`, `meta`, `jsonLd[]`, `tables[]` (`{ headers, rows: [{ header: value }] }`), and `headings[]`. If the client supports **sampling**, `data` holds the schema filled by the client's model and `method` is `"sampling"`; otherwise the schema is returned as a hint next to the content (`method: "dom"`).

### links

```js
links({ url: "https://docs.example.com", sameOrigin: true, match: "/api/" })
```

### interact

```js
interact({
  url: "http://localhost:3000/signup",
  actions: [
    { type: "fill", selector: "input[name='email']", value: "test@example.com" },
    { type: "press", selector: "input[name='email']", value: "Enter" },
    { type: "wait", selector: ".welcome" }
  ]
})
```

Password values are never echoed back. If an action navigates to a host your policy denies, the result is withheld.

## Signed-in pages

**Option A: dedicated profile (recommended)**

```bash
npx @network101/localmcp login https://dashboard.stripe.com
```

A browser window opens on `~/.localmcp/profile`. Sign in to whatever your agent should read, then close the window. With the default `browser.profile: "auto"`, localmcp uses that profile from then on. Chromium locks a profile to one process. If two clients run localmcp at once, the second one falls back to an ephemeral session and says so in its output.

**Option B: your own Chrome**

Start Chrome with `--remote-debugging-port=9222`, then:

```json
{ "browser": { "cdpEndpoint": "http://localhost:9222" } }
```

localmcp opens its own tabs in your existing session and never closes your browser.

> Either way the agent can read anything those sessions can. So while a signed-in session is in use, `interact` is **off by default** (`policy.allowInteract: "auto"`): the agent reads as you but can't click or type as you until you set `allowInteract: true`. Pair this with `policy.allow` (below).

## Browsers

| Browser | How | Status |
|---|---|---|
| Chromium (bundled) | default | ✓ |
| Google Chrome, Microsoft Edge | `"channel": "chrome"` / `"msedge"` | ✓ |
| Firefox | `"engine": "firefox"` | ✓ |
| WebKit (Safari's engine) | `"engine": "webkit"` | ✓ |
| Your running Chrome, Edge, Brave, Arc, Vivaldi, Opera | `"cdpEndpoint"` | ✓ |
| Your running Firefox | WebDriver BiDi attach | Roadmap |
| Your running Safari | `safaridriver` | Roadmap |
| Tabs in your everyday browser, no flags | extension bridge | Roadmap |

Firefox and WebKit need a one-time `npx playwright install firefox webkit`. Sign in per engine with `npx @network101/localmcp login <url> --browser firefox`.

## Configuration

`npx @network101/localmcp init` writes `.localmcp.json`. Global defaults can live in `~/.config/localmcp/config.json`; project config wins.

```jsonc
{
  "browser": {
    "timeout": 30000,
    "headless": true,
    "engine": "chromium",         // "chromium" | "firefox" | "webkit"
    "channel": null,              // chromium only: "chrome" | "msedge" | …
    "profile": "auto",            // "auto" | "persistent" | "ephemeral"
    "cdpEndpoint": null           // e.g. "http://localhost:9222"
  },
  "distill": {
    "maxTokens": 4000,            // default budget for browse/extract/interact
    "includeLinks": true,
    "includeImages": false,
    "publisherMarkdown": true     // prefer markdown the site serves for agents
  },
  "policy": {
    "allow": [],                  // host globs; empty = any. e.g. ["*.stripe.com", "localhost:*"]
    "deny": [],                   // checked first
    "allowInteract": "auto",      // "auto": off while signed in · true · false (read-only)
    "allowFileUrls": false
  },
  "limits": {
    "maxSessionsPerDay": 100,     // local circuit breaker against runaway agents
    "maxTokensPerDay": 1000000
  }
}
```

Host globs: `example.com` (exact host, any port), `*.example.com` (subdomains, not the apex), `localhost:3000`, `localhost:*`.

## Security model

- **Nothing is relayed.** Pages are fetched and distilled by a browser on your machine, then go only to the model your agent already uses. No telemetry, no account.
- **Policy at the boundary.** Every URL is checked before a browser is touched, and again after redirects or actions that change origin. Deny rules also apply to every request a page makes — images, iframes, scripts, fetches — including when attached to your own browser. Only `http(s)` is allowed by default; `file:`, `javascript:`, `chrome:`, `data:` are refused.
- **Prompt-injection fence.** Page content comes back inside `<untrusted-page-content>` tags, and the server's `instructions` tell the model to treat it as data. Pages that try to close the fence early are neutralised. This *reduces* injection risk; it doesn't eliminate it. Keep `interact` confirmations on in your client.
- **Circuit breaker.** Daily session and token caps (local SQLite at `~/.localmcp/usage.db`). `npx @network101/localmcp usage` shows totals. `LOCALMCP_NO_LIMIT=1` overrides.

## MCP protocol surface

| Feature | Support |
|---|---|
| `tools` with `title`, `annotations`, `outputSchema`, `structuredContent` | ✓ |
| `isError` tool results | ✓ |
| Server `instructions` | ✓ |
| `notifications/progress` (when the client sends a `progressToken`) | ✓ |
| `sampling/createMessage` (used by `extract` when the client supports it) | ✓ |
| `resources` — diff snapshots at `localmcp://snapshot/{url}` | ✓ |
| Transport | stdio |

## CLI

```
localmcp                 Start the MCP server on stdio
localmcp init            Create .localmcp.json and print setup for your MCP clients
localmcp login [url]     Sign in once on the persistent profile
localmcp usage           Today's and this week's usage
localmcp --version
```

## Token benchmark

`npm run bench` measures named public pages four ways — rendered HTML, a Playwright MCP `browser_snapshot`, `browse` with no budget, and `browse` with the 4,000-token default — and writes [benchmarks/results.md](benchmarks/results.md). On 2026-10-01:

| Page | Rendered HTML | Playwright MCP snapshot | localmcp (full) | localmcp (default) |
|---|---:|---:|---:|---:|
| Stripe API reference | 428,457 | 30,724 | 482 | 482 |
| GitHub REST: Issues | 330,083 | 87,350 | 20,228 | 4,002 |
| Next.js docs | 159,921 | 19,721 | 799 | 799 |
| Wikipedia: Model Context Protocol | 109,268 | 20,288 | 6,387 | 3,991 |
| Python: json module | 29,700 | 25,381 | 8,624 | 3,996 |
| Hacker News front page | 8,539 | 12,207 | 4,023 | 3,945 |

Tokens ≈ characters ÷ 4 in every column. Savings range from 2× (a page that is already mostly text) to hundreds of times (a site serving its own markdown); `focus` and `maxTokens` cap any page at the budget you choose. Tool schemas: localmcp 1,352 tokens, Playwright MCP 5,072 — Claude Code, Cursor and Codex load schemas on demand, so this mainly matters for clients that don't.

## Development

```bash
npm install
npx playwright install chromium
npm test          # vitest — unit, browser, and end-to-end MCP protocol tests
npm run build
```

## Name

localmcp was previously published as `browsermcpai` (and `headlessdev` before that). The old config files, state directory and env override keep working; see the [changelog](CHANGELOG.md).

It is not affiliated with Browser MCP (browsermcp.io), a Chrome-extension project, nor with the `local-mcp` and `@localmcp/*` packages on npm, which give ChatGPT and Claude shell and file access to your machine. localmcp is the opposite kind of tool: a scoped, read-only browser.

## License

MIT

Maintenance

ActivityMaintained
ResponsivenessNo issues