Automate your browser from TypeScript.
@sessionat/sdk is the typed client for the browser's built-in MCP server. Read your local visit analytics, manage workspaces, and drive any page from Node. Zero dependencies, connected over local loopback, MIT licensed.
npm i @sessionat/sdkConnect in one line. Read data, then drive the page.
connect() finds your running browser automatically by reading the local discovery file it writes (loopback port plus a bearer token), then health-probes each candidate to pick the live one.
import { Sessionat } from "@sessionat/sdk";
// Zero-config: auto-discovers your running browser over local loopback.
const browser = await Sessionat.connect();
// Your private, local analytics (read-only, no approval needed):
const top = await browser.analytics.topSites({ range: "7d", orderBy: "active_seconds" });
console.log(top.top_sites); // [{ host, visit_count, active_seconds, category }]
// Drive the live browser (one-time approval in chrome://sessionat-mcp/):
await browser.tabs.open("https://news.ycombinator.com");
await browser.page.waitFor({ text: "Hacker News" });
const shot = await browser.page.screenshot(); // { data, mimeType }Three surfaces, one typed client.
Top sites, focus categories, time-of-day buckets, and full visit history. Everything is stored locally and nothing is uploaded, so analytics reads need no approval.
analytics · visitsList, inspect, and create Arc-style workspaces, and read the tabs inside each one. Wire your own scripts into the same workspace model the browser uses.
workspacesOpen, focus, and close tabs, navigate, read page text, click, type, scroll, wait, and screenshot. It pierces shadow DOM, cross-origin iframes, and canvas apps.
tabs · pageAuto-discovery, or be explicit.
In most cases Sessionat.connect() just works. For CI, multiple profiles, or remote setups you can pass an endpoint and token directly, or point at a profile or discovery file. You can also configure it entirely from the environment.
| SESSIONAT_MCP_URL + SESSIONAT_MCP_TOKEN | Use this exact endpoint and skip discovery. |
| SESSIONAT_MCP_DISCOVERY | Path to an mcp.json discovery file. |
| SESSIONAT_PROFILE_DIR | A profile dir; reads <dir>/mcp.json. |
// Explicit endpoint (copy from chrome://sessionat-mcp/, "Other" tab):
await Sessionat.connect({ url: "http://127.0.0.1:54321/mcp", token: "..." });
// Or point at a specific profile / discovery file:
await Sessionat.connect({ profileDir: "/path/to/Sessionat/Default" });
await Sessionat.connect({ discoveryFile: "/path/to/mcp.json" });Local-only by design
The connection is loopback-only (127.0.0.1) and bearer-token authenticated. Your visit data never leaves your device, and the SDK makes no outbound calls of its own beyond the local browser.
Reads are open. Writes need one-time approval.
Reading analytics, visits, and workspaces is open. Every write or automation tool (opening tabs, clicking, typing, even listing open tabs) is gated behind a one-time, per-client approval. It is a deliberate guard against prompt-injection. The first such call throws, you approve the client once, and retry.
import { SessionatWriteApprovalError } from "@sessionat/sdk";
try {
await browser.tabs.open("https://example.com");
} catch (err) {
if (err instanceof SessionatWriteApprovalError) {
// Open chrome://sessionat-mcp/, enable write tools for this client, retry.
console.error(err.message);
}
}Five namespaces on the connected client.
Sessionat.connect() returns a typed browser with the namespaces below. There is also a low-level escape hatch: browser.listTools(), browser.callTool(name, args), and browser.request(method) for raw JSON-RPC.
Your 100%-local browsing analytics. Read-only, no approval needed.
| topSites({ range?, orderBy?, limit?, workspaceId? }) | Most-visited hosts with visit counts, active seconds, and category. |
| categories({ range?, workspaceId? }) | Time split across content categories (work, social, reference, ...). |
| buckets({ bucket, range?, workspaceId? }) | Activity grouped by hour, day of week, or day. |
Search and read your local visit history. Read-only.
| list({ range?, limit?, workspaceId? }) | Recent visits, newest first. |
| search(query, { range?, limit?, workspaceId? }) | Full-text search across visited pages. |
| forHost(host, { range?, limit?, workspaceId? }) | Every visit to a single host. |
Read your Arc-style workspaces. Creating one is write-gated.
| list() | All workspaces plus the active id. |
| active() | The active workspace and its open tabs. |
| get(workspaceId) | One workspace by id. |
| create({ name, color?, icon? }) * | Create a new workspace. |
Open, focus, and close tabs in the live browser. Write-gated.
| list() * | Open tabs across the browser. |
| active() * | The currently focused tab. |
| open(url, { background? }) * | Open a URL in a new tab. |
| focus(index) * | Switch to a tab by index. |
| close(index) * | Close a tab by index. |
Drive the active page. Pierces shadow DOM and cross-origin iframes. Write-gated.
| navigate(url) * | Load a URL in the active tab. |
| text({ maxChars?, rootSelector?, frameUrlMatch? }) * | Read the visible page text. |
| click({ selector?, text?, textExact?, frameUrlMatch? }) * | Click by selector or visible text. |
| type({ selector?, fieldLabel?, text, submit?, frameUrlMatch? }) * | Type into a field, optionally submit. |
| waitFor({ selector?, text?, textExact?, timeoutMs? }) * | Wait for an element or text to appear. |
| scroll({ selector?, dx?, dy?, position? }) * | Scroll the page or an element. |
| outline({ limit?, rootSelector?, frameUrlMatch? }) * | Structured outline of interactive elements. |
| screenshot({ maxWidth? }) * | Capture the viewport. Returns { data, mimeType }. |
| pressKey(key, { modifiers? }) * | Send a real, trusted key event. |
| typeKeys(text) * | Type text as trusted key events (canvas apps like Docs, Figma). |
Tiny, typed, and open.
Get the browser, then start scripting.
The SDK talks to the free Sessionat browser on your Mac. Download it, enable the MCP server, and run the quickstart.