Skip to main content
Glama
adrianba

edge-history-mcp

by adrianba
README.md
# edge-history-mcp

A stdio [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that
exposes **Microsoft Edge browsing history** across multiple profiles.

It lets an MCP client:

- **List Edge profiles** by their friendly names.
- **Fetch history for a given day** for a given profile, returning one entry per
  page visit with timestamp, URL, title, and other metadata.
- **Summarize a day's browsing** hour by hour as **domains only**, without
  exposing full URLs.

The server only reads Edge data and never modifies it. Because Edge keeps the
live `History` database locked while running, the server copies it (together with
any rollback-journal/WAL sidecar files) to a private temporary directory and
reads the copy — so it works even while Edge is open, and SQLite can recover a
consistent snapshot if the copy was taken mid-write.

## Quick start (GitHub Copilot CLI)

You don't need to clone this repo. With [uv](https://docs.astral.sh/uv/)
installed, register the server in your **user** config with a single command —
`uvx` builds and runs it on demand straight from GitHub:

```sh
copilot mcp add edge-history -- uvx --from git+https://github.com/adrianba/edge-browser-mcp edge-history-mcp
```

Then, from any directory:

```sh
copilot mcp list      # edge-history should appear under User servers
copilot              # start a session and ask it to use the edge-history tools
```

> The source/repo is **`edge-browser-mcp`** and the command/package is
> **`edge-history-mcp`** — both appear in the command above (`--from …edge-browser-mcp`
> is the source, the trailing `edge-history-mcp` is the command to run).

To pin to a released version (recommended for stability), append a tag:

```sh
copilot mcp add edge-history -- uvx --from git+https://github.com/adrianba/edge-browser-mcp@v0.2.1 edge-history-mcp
```

## Requirements

- Windows with Microsoft Edge installed.
- [uv](https://docs.astral.sh/uv/) (Python is managed by uv; no manual venv needed).

## Tools

### `list_profiles()`

Lists available Edge browsing profiles. Returns a list of objects:

| Field        | Description                                              |
| ------------ | -------------------------------------------------------- |
| `name`       | Friendly profile name (from Edge `Local State`).         |
| `directory`  | On-disk profile directory id (e.g. `Default`, `Profile 3`). |
| `is_default` | `true` for the default profile.                          |

Use `name` (or `directory`) as the `profile` argument to `get_history`.

### `get_history(profile, date, start_time=None, end_time=None, limit=10000, offset=0)`

Returns a page of per-visit history entries for a profile on a single day. A busy
day can hold thousands of visits, so the result is **paginated** and can be
narrowed to an intra-day **time window**.

- `profile` — friendly name from `list_profiles`, or a directory id.
- `date` — `YYYY-MM-DD`. Day boundaries are interpreted in the **local machine
  timezone** (DST-aware). The server determines the local timezone from the OS
  (via `tzlocal`) rather than relying on Python's process-local timezone, which
  can be incorrect in some Windows/Scout environments. Override with the
  `EDGE_HISTORY_TIMEZONE` environment variable (IANA name, e.g.
  `America/Los_Angeles`).
- `start_time` — optional lower bound within the day, `HH:MM` (24-hour, local).
  Defaults to the start of the day.
- `end_time` — optional upper bound within the day, `HH:MM` (local, **exclusive**).
  Defaults to the end of the day. Must be later than `start_time`.
- `limit` — optional maximum number of entries to return per page (default
  `10000`, capped at `50000`; non-positive values fall back to the default).
- `offset` — optional number of entries to skip from the start of the window, for
  paging through large results (default `0`).

The result is an object:

| Field         | Description                                                   |
| ------------- | ------------------------------------------------------------- |
| `entries`     | List of visit entries (see below), ordered ascending by time. |
| `offset`      | The offset that was applied.                                  |
| `limit`       | The page size that was applied.                               |
| `has_more`    | `true` if more entries follow this page.                      |
| `next_offset` | Offset to request the next page, or `null` when `has_more` is `false`. |

Each entry in `entries` contains:

| Field         | Description                                                   |
| ------------- | ------------------------------------------------------------- |
| `visit_time`  | Visit time as a local ISO-8601 timestamp.                     |
| `url`         | Visited URL.                                                  |
| `title`       | Page title (may be empty).                                    |
| `visit_count` | Total number of visits to this URL.                           |
| `typed_count` | Number of times the URL was typed.                            |
| `transition`  | Page-transition type (`link`, `typed`, `reload`, …).          |
| `url_id`      | Internal `urls.id`.                                           |
| `visit_id`    | Internal `visits.id`.                                         |

**Fetching part of a day.** Pass `start_time`/`end_time` to pull a narrow window
instead of the whole day, e.g. `get_history("Work", "2024-03-15", start_time="09:00",
end_time="10:00")`.

**Paging through a large day.** Start with `offset=0`; if the result has
`has_more=true`, call again with `offset=next_offset` until `has_more` is `false`.

> **Privacy note.** `get_history` returns URLs **verbatim**, and URLs can embed
> auth tokens, session ids, password-reset links or pre-signed URLs. For
> questions like "what did I browse today?", prefer `get_history_summary` below
> and only reach for `get_history` when specific URLs or titles are needed.

### `get_history_summary(profile, date, start_time=None, end_time=None, group_by="hour", limit=10000, offset=0, include_titles=True, max_titles_per_site=3)`

Returns an **aggregated, privacy-preserving** view of the same window as
`get_history`: visits grouped into local-time hour buckets with **domains only**,
plus a small sample of page titles per domain for context.

It intentionally returns **no full URLs, query strings or fragments** — only
normalized domains (e.g. `www.github.com` -> `github.com`) and bounded page-title
samples. Only `http`/`https` visits are aggregated; `file:`, `edge:`, `chrome:`,
extension and other non-web URLs are excluded (and counted so clients can explain
the omission). Full per-visit URLs are available only through `get_history`, and
may contain sensitive data.

- `profile`, `date`, `start_time`, `end_time`, `limit`, `offset` — identical
  semantics to `get_history` (same profile resolution, DST-aware local day/window
  boundaries, limit cap and pagination; `limit`/`offset` page over the underlying
  visit rows).
- `group_by` — bucket granularity. Currently only `"hour"` is supported.
- `include_titles` — include `sample_titles` for each domain (default `true`).
  Set to `false` for domains and counts only.
- `max_titles_per_site` — maximum sample titles per domain per bucket (default
  `3`, capped at `10`). Values `<= 0` return empty title lists.

The result is an object:

| Field                     | Description                                                        |
| ------------------------- | ------------------------------------------------------------------ |
| `profile`                 | Resolved profile friendly name.                                    |
| `date`                    | The requested date.                                                |
| `group_by`                | The bucket granularity applied (`hour`).                           |
| `timezone`                | IANA timezone used for bucketing.                                  |
| `entries_considered`      | Visit rows examined in this page.                                  |
| `web_entries`             | Rows counted in the buckets.                                       |
| `non_web_entries_skipped` | Rows skipped because they were not `http`/`https`.                 |
| `buckets`                 | Hour buckets, ordered ascending (see below).                       |
| `offset` / `limit`        | The paging window that was applied.                                |
| `has_more`                | `true` if more visit rows follow this page.                        |
| `next_offset`             | Offset for the next page, or `null` when `has_more` is `false`.    |

Each bucket contains:

| Field              | Description                                                              |
| ------------------ | ------------------------------------------------------------------------ |
| `hour`             | Local ISO-8601 timestamp of the start of the hour.                       |
| `label`            | Friendly hour label, e.g. `8 AM`.                                        |
| `total_web_visits` | Number of web visits in the bucket.                                      |
| `sites`            | `{ "domain", "visits", "sample_titles" }` entries, sorted by visits descending then domain ascending. |

`sample_titles` holds up to `max_titles_per_site` page titles seen for that domain
in that bucket: deduplicated, in first-seen order, whitespace-trimmed, empty titles
skipped, and truncated to 160 characters. Titles are page text only — never URLs.

Example:

```json
{
  "profile": "Profile 2",
  "date": "2026-08-05",
  "group_by": "hour",
  "timezone": "America/Los_Angeles",
  "entries_considered": 206,
  "web_entries": 206,
  "non_web_entries_skipped": 0,
  "buckets": [
    {
      "hour": "2026-08-05T08:00:00-07:00",
      "label": "8 AM",
      "total_web_visits": 48,
      "sites": [
        {
          "domain": "map.pscleanair.org",
          "visits": 47,
          "sample_titles": ["Puget Sound Clean Air Agency Map"]
        },
        {
          "domain": "inciweb.nwcg.gov",
          "visits": 1,
          "sample_titles": ["InciWeb - Incident Information System"]
        }
      ]
    }
  ],
  "offset": 0,
  "limit": 10000,
  "has_more": false,
  "next_offset": null
}
```

## MCP client configuration

For MCP clients that read a JSON config (`mcpServers`), the recommended entry runs
the published server from GitHub with `uvx` — no clone required:

```json
{
  "mcpServers": {
    "edge-history": {
      "command": "uvx",
      "args": [
        "--from",
        "git+https://github.com/adrianba/edge-browser-mcp",
        "edge-history-mcp"
      ]
    }
  }
}
```

If you have the repo checked out and prefer running from source, use `uv` with a
`cwd` pointing at the repo instead:

```json
{
  "mcpServers": {
    "edge-history": {
      "command": "uv",
      "args": ["run", "edge-history-mcp"],
      "cwd": "C:\\path\\to\\edge-browser-mcp"
    }
  }
}
```

### GitHub Copilot CLI

The recommended setup is the one-line **user-config** install shown in
[Quick start](#quick-start-github-copilot-cli):

```sh
copilot mcp add edge-history -- uvx --from git+https://github.com/adrianba/edge-browser-mcp edge-history-mcp
```

This works from any directory and uses `"type": "local"` (a stdio server Copilot
launches as a subprocess). Verify with `copilot mcp list` (it appears under
**User servers**) and inspect it with `copilot mcp get edge-history`.

Copilot CLI loads MCP definitions from several locations:

| Source    | Location                                         |
| --------- | ------------------------------------------------ |
| Workspace | `.mcp.json` or `.github/mcp.json` (this repo)    |
| User      | `~/.copilot/mcp-config.json` (the `add` command) |
| Plugin    | installed plugins that bundle MCP servers         |

#### Workspace `.mcp.json` (local development only)

This repo ships a workspace `.mcp.json` that runs the server **from source**
(`uv run edge-history-mcp`). It auto-loads only when you start `copilot` from the
repo directory, so it's meant for **developing/testing this project** — not for
everyday use. End users should prefer the `uvx`-from-git command above.

```json
{
  "mcpServers": {
    "edge-history": {
      "type": "local",
      "command": "uv",
      "args": ["run", "edge-history-mcp"],
      "tools": ["*"]
    }
  }
}
```

#### Testing this project in a Copilot CLI session (from source)

1. Pre-flight in a normal shell:

   ```sh
   cd C:\Repos\edge-browser-mcp
   uv sync                  # install dependencies
   uv run edge-history-mcp  # optional: confirm it starts on stdio (Ctrl+C to exit)
   copilot mcp list         # expect: Workspace servers: edge-history (local)
   ```

2. Launch Copilot CLI **from the repo directory** so `.mcp.json` is loaded, and
   trust the folder when prompted:

   ```sh
   copilot
   ```

3. Inside the session, confirm the server and tools are available:

   ```
   /mcp        # MCP UI — edge-history should be listed/enabled
   /env        # shows loaded MCP servers and tools
   ```

4. Exercise the tools with natural-language prompts (approve the first tool use):

   ```
   List my Edge browser profiles using the edge-history MCP server.
   Using edge-history, get my browsing history for profile "Profile 1" on 2026-06-20.
   Get the first 5 history entries for the "Google Drive" profile on 2026-06-20.
   ```

5. Check error handling:

   ```
   Using edge-history, get history for a profile called "DoesNotExist" on 2026-06-20.
   Get history for "Profile 1" on 06/20/2026.   (wrong format -> clean error)
   ```

To isolate the test from your real Copilot config, point Copilot at a throwaway
home first: `$env:COPILOT_HOME = "C:\Temp\copilot-test"`. The workspace
`.mcp.json` still loads by working directory.

## Privacy & security

Browsing history is sensitive personal data. Be aware:

- The server exposes the **full history of every profile** to whichever MCP
  client launches it. Only enable it with clients you trust.
- `get_history` returns URLs **verbatim**; they may embed secrets (auth/session
  tokens, password-reset links, pre-signed URLs). Treat tool output as sensitive
  and only request full URLs when they are actually needed.
- `get_history_summary` is the lower-risk default: it deliberately returns
  **domains only**, dropping paths, query strings and fragments, and skips
  non-web (`file:`, `edge:`, `chrome:`, extension) URLs entirely. It adds a small
  sample of page titles per domain for context; titles can still be descriptive,
  so use `include_titles=False` if even that is too much. Full per-visit URLs are
  only ever returned by `get_history`.
- A plaintext copy of the `History` database is written to a temporary directory
  for the duration of each query and then deleted.
- Reads are strictly read-only; Edge's own data is never modified.

## Notes

- By default the server reads Edge data from
  `%LOCALAPPDATA%\Microsoft\Edge\User Data`. Override with the
  `EDGE_USER_DATA_DIR` environment variable (useful for testing).
- `Guest Profile` and `System Profile` are excluded from `list_profiles`.
- Chromium stores timestamps as microseconds since 1601-01-01 UTC; the server
  converts these to local time using the OS timezone (determined via `tzlocal`,
  not Python's process-local timezone). Override with `EDGE_HISTORY_TIMEZONE`.
- The MCP SDK dependency is pinned to `mcp[cli]>=1.2.0,<2.0.0` because the
  server uses the MCP 1.x `FastMCP` API (`mcp.server.fastmcp`). MCP 2.x
  removed this module; do not upgrade until the server is migrated to the 2.x
  API.

## Development

Clone the repo and use [uv](https://docs.astral.sh/uv/):

```sh
uv sync                  # create .venv and install deps (incl. dev)
uv run edge-history-mcp  # run the server on stdio (Ctrl+C to exit)
uv run pytest            # run the test suite
uv build                 # build sdist + wheel into dist/
```

`uv run edge-history-mcp` starts the server on **stdio**; it is normally launched
by an MCP client rather than run directly.

Tests build a synthetic Edge-shaped SQLite database in a temp directory, so they
do not touch your real Edge installation. CI runs them on every push/PR (see
`.github/workflows/ci.yml`).

TDQS

A4.8/5.0

Scored across 3 tools

Disambiguation5/5

The three tools are clearly distinct: list_profiles handles profile enumeration, get_history returns raw detailed visit entries, and get_history_summary provides an aggregated domain-level view. The descriptions explicitly explain when to use each, eliminating ambiguity.

Naming Consistency5/5

All tool names follow a consistent verb_noun snake_case pattern. list_profiles and get_history use clear action-object phrasing, and get_history_summary extends get_history logically. No mixed conventions or vague verbs.

Tool Count5/5

With only three tools, the server is tightly scoped to browsing history retrieval. Each tool earns its place: profile discovery, raw history access, and summarization. The count is neither too thin nor bloated for the purpose.

Completeness5/5

The server covers the full read-only lifecycle for Edge history: discovering profiles, fetching detailed history with pagination and time filtering, and obtaining summaries. No obvious gaps exist within the narrow domain; the tools together handle typical use cases.

Maintenance

ActivityNo data
ResponsivenessNo issues