edge-history-mcp
# 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
Scored across 3 tools
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.
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.
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.
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.