Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
13e74b2
fix(mcp): recover stale bridge connections and report lifecycle causes
LakshmanTurlapati Sep 29, 2026
0cea443
Merge remote-tracking branch 'origin/main' into diagnose-random-bridg…
LakshmanTurlapati Sep 29, 2026
2e7628b
fix(mcp): confine HTTP transport to loopback clients
LakshmanTurlapati Sep 29, 2026
0fa7be7
fix(actions): avoid duplicate clicks and uncertain mutation retries
LakshmanTurlapati Sep 29, 2026
a1f14af
fix(mcp): resolve action wire verbs through the tool registry
LakshmanTurlapati Sep 29, 2026
e984f03
fix(editing): make text insertion positional and at most once
LakshmanTurlapati Sep 29, 2026
38eee85
fix(selectors): preserve CSS conditions and recheck cached matches
LakshmanTurlapati Sep 29, 2026
916802f
fix(recovery): bound page probes and reinjection on hung tabs
LakshmanTurlapati Sep 29, 2026
7d48375
fix(vault): distinguish locked and unconfigured lists
LakshmanTurlapati Sep 29, 2026
afe000d
fix(diagnostics): report attached profile identity and window state
LakshmanTurlapati Sep 29, 2026
5d059d2
docs(guide): clarify X actions, onboarding, and bridge setup
LakshmanTurlapati Sep 29, 2026
30c4104
fix(actions): stop late and repeated mutations after uncertain delivery
LakshmanTurlapati Sep 29, 2026
d5c1ed2
fix(diagnostics): expose extension attachment in local health
LakshmanTurlapati Sep 29, 2026
5df2402
fix(guide): constrain X guidance to genuine hosts and full checks
LakshmanTurlapati Sep 29, 2026
c212aa4
test(ci): run issue regressions and isolate bridge smoke pairing
LakshmanTurlapati Sep 29, 2026
714f014
fix(actions): reach canvas editors on replace and release the debugge…
LakshmanTurlapati Sep 29, 2026
e20413f
fix(bridge): clear the attachment and journal listener loss on hub ab…
LakshmanTurlapati Sep 29, 2026
6feda7b
test(actions): run the Chrome editing fixture on CI's Node 20
LakshmanTurlapati Sep 29, 2026
f7048ae
fix(bridge): release hung injections and retry CDP failures before te…
LakshmanTurlapati Sep 29, 2026
87830b8
fix(bridge): report undelivered actions as not executed
LakshmanTurlapati Sep 29, 2026
d655a43
chore(showcase): refresh crawler file dates
LakshmanTurlapati Sep 29, 2026
f3e0ab3
fix(mcp): serve HTTP requests on the IPv6 loopback
LakshmanTurlapati Oct 1, 2026
25aaa0f
fix(actions): fall back to plain insertion when a Docs paste inserted…
LakshmanTurlapati Oct 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ The independently published `fsb-mcp-server` npm package keeps its own semver ch

### Fixed

- **The bridge no longer dies until you restart Chrome.** The local bridge keeps a single extension socket, and it was freed only when that socket closed itself. An extension whose service worker died without the socket closing therefore held the slot indefinitely, and every legitimate reconnect was refused — one episode in the field ran 13,328 refusals over roughly 25 hours, ending only when the browser quit. The extension could not learn from any of it: its `onclose` handler discarded the close code, and because the WebSocket upgrade *succeeded* before the refusal arrived, `onopen` reset the reconnect backoff on every cycle, so it retried at a flat two seconds forever. The bridge now reaps a socket that stops answering both protocol pings and its own heartbeat; the extension reads the close code, escalates its backoff toward the 30-second ceiling instead of flattening it, and drops a pairing credential the bridge has explicitly rejected rather than replaying it until the browser exits.
- **Keepalive actually detects a dead peer now.** The extension sent a ping every 25 seconds and never looked at the reply; the reply itself was discarded by a handler that only accepted the delegation heartbeat's three-key form. Neither end could notice a half-open socket. Unanswered pings are now counted and close the socket, which hands it to the existing reconnect path.
- **Servers exit when their host does.** `fsb-mcp-server` listened only for SIGTERM and SIGINT, so a host that exits by closing the pipe left a fully working server running forever. Because the bridge port is claimed once by a bind race and never re-attempted, the bridge owner drifted into being the oldest surviving orphan rather than a process anyone was still talking to. The server now shuts down when its stdio pipe closes.
- **A pairing mismatch says so.** An extension id that does not match the pinned pairing was refused with a bare HTTP 403 — no WebSocket, no close code, no reason — which is why the failure so often ended in a reinstall that could not fix it. The refusal now names both origins and the `pair --reset` cure, and `doctor` reports an authorization layer instead of blaming the browser.
- **The bridge keeps a journal.** Refusals, revocations, replacements, reaps, and closes are recorded in `~/.fsb/agent-runtime/bridge-events.jsonl`, coalesced so a retry loop leaves one line a minute rather than tens of thousands. Previously every bridge diagnostic went to the stderr of whichever server won the port race, which was frequently a session that had already exited.
- **`sw_evicted` no longer hides the real cause.** Authorization failures were reported to callers as a service-worker eviction, sending everyone to restart the browser. The recovery is unchanged — the task really was interrupted — but the response now carries a `disconnect_reason` that distinguishes a revoked credential from an evicted worker.

- **Dashboard QR pairing failures are visible again.** Scanning a pairing code that was expired, already used, or otherwise rejected reported nothing at all: the "Connecting..." state replaced the scan panel markup, so the error was written into a node that had already been removed from the DOM, and the dashboard then switched to the Paste Key tab. Every failure now renders its localized reason, keeps the user on the Scan tab, and restarts the camera so a regenerated code can be scanned directly.

## v0.9.91 — Version Metadata Alignment — 2026-07-14
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,8 @@ Start with simple tasks such as:

Reload the extension from `chrome://extensions/` after local code changes. Reload open tabs after the extension reloads so content scripts re-inject.

For an unpacked build, record the ID on `chrome://extensions/`. A different install path or profile can produce a different ID; if `doctor` reports `ORIGIN_PIN_MISMATCH`, run `npx -y fsb-mcp-server@latest pair --reset` and pair the new installation. One browser profile attaches to the local bridge at a time. `doctor` and `status` show the attached extension ID, version, install instance, connection time, and normal-window count. The MCP server and extension have independent versions. Branded Chrome 137 removed the `--load-extension` flag; use **Load unpacked** in Chrome, or use [Chrome for Testing or Chromium when a command-line flag is required](https://groups.google.com/a/chromium.org/g/chromium-extensions/c/1-g8EFx2BBY/m/S0ET5wPjCAAJ).

### First Run Checklist

1. Open the FSB control panel from the extension.
Expand Down Expand Up @@ -287,6 +289,8 @@ Optional Streamable HTTP mode exposes:
http://127.0.0.1:7226/mcp
```

The HTTP endpoint binds to loopback only and rejects requests with an `Origin` header or a foreign `Host`. Use the printed local endpoint from an MCP client. If a tab stops responding, page reads return `PAGE_UNRESPONSIVE`; `navigate` and `close_tab` remain available. Inspect page state before repeating any action reported as `outcome: "unknown"` and `mayHaveExecuted: true`.

### One Command Install

```bash
Expand Down
4 changes: 4 additions & 0 deletions extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@

After code changes, reload the extension from `chrome://extensions` and refresh any open tabs so content scripts re-inject.

Copy the extension ID shown on `chrome://extensions` when pairing an unpacked build. Its ID can differ from the Chrome Web Store ID and can change if you reinstall it from another directory. Run `npx -y fsb-mcp-server@latest pair --reset` if `doctor` reports `ORIGIN_PIN_MISMATCH`, then pair the new installation. `doctor` also shows a persistent install instance ID, the extension version, and the normal-window count so you can identify which browser profile holds the bridge. One extension/profile attaches to the local bridge at a time; another profile may replace it. The MCP server and extension have independent version numbers.

In branded Chrome 137 and later, the `--load-extension` command-line flag no longer loads unpacked extensions. Use the **Load unpacked** button above. [Chrome for Testing and Chromium retain the flag](https://groups.google.com/a/chromium.org/g/chromium-extensions/c/1-g8EFx2BBY/m/S0ET5wPjCAAJ).

## Google Sheets Development

Google Sheets capabilities reuse an already signed-in, agent-owned spreadsheet tab. They require no Google Cloud client ID, consent prompt, token setup, or Sheets-specific MCP update. Reload the unpacked extension after changes, refresh the open Sheet, and reconnect the existing MCP bridge. See [Google Sheets signed-in session integration](../docs/google-sheets-api.md) for the bounded operation contract and UAT gates.
Expand Down
11 changes: 10 additions & 1 deletion extension/ai/agent-loop.js
Original file line number Diff line number Diff line change
Expand Up @@ -2424,8 +2424,17 @@ async function runAgentIteration(sessionId, options) {
? getGuideForTask('', 'https://' + domain)
: null;
if (guide) {
var guideGuidance = JSON.stringify({
selectors: guide.selectors || {},
workflows: {
createPost: guide.workflows && guide.workflows.createPost,
replyToPost: guide.workflows && guide.workflows.replyToPost
},
warnings: Array.isArray(guide.warnings) ? guide.warnings.slice(0, 6) : [],
guidance: typeof guide.guidance === 'string' ? guide.guidance.slice(0, 1600) : ''
}).slice(0, 5000);
result = { success: true, hadEffect: false, error: null, navigationTriggered: false,
result: { domain: domain, site: guide.site || guide.name || domain, guidance: JSON.stringify(guide.selectors || guide) } };
result: { domain: domain, site: guide.site || guide.name || domain, guidance: guideGuidance } };
} else {
result = { success: true, hadEffect: false, error: null, navigationTriggered: false,
result: { domain: domain, guidance: 'No site guide available for ' + domain + '. Use get_page_snapshot and get_dom_snapshot to discover elements.' } };
Expand Down
17 changes: 14 additions & 3 deletions extension/ai/tool-definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,12 +242,13 @@ const TOOL_REGISTRY = [

withVisualSessionFields({
name: 'type_text',
description: 'Type text into an input field by selector. When to use: to fill text inputs, search boxes, or text areas. Use clear_input first if the field already has text. Returns confirmation of typed text. Related: clear_input (clear field before typing), press_enter (submit after typing), get_dom_snapshot (find input selectors). Multi-agent: agent-scoped tabs; cross-agent reject with TAB_NOT_OWNED; cap configurable (default 8, 1-64). Pass tab_id only when this agent owns multiple tabs; auto-resolves otherwise.',
description: 'Replace text in an editable field by selector. Set clear_first to false to append. Returns the rendered text after insertion. Related: clear_input, press_enter, get_dom_snapshot. Multi-agent: agent-scoped tabs; cross-agent reject with TAB_NOT_OWNED; cap configurable (default 8, 1-64).',
inputSchema: {
type: 'object',
properties: {
selector: { type: 'string', description: 'CSS selector or element ref for the input field (e.g., "#email", "input[name=search]", or "e12" from get_dom_snapshot)' },
text: { type: 'string', description: 'Text to type into the field' },
clear_first: { type: 'boolean', description: 'Replace existing text (default true); false appends at the end.' },
tab_id: { type: 'number', description: 'Optional. Tab id this action targets. Omit when the calling agent owns exactly one tab; pass to disambiguate when the agent owns multiple. Single-tab agents and legacy popup/sidepanel/autopilot do not need to pass this.' }
},
required: ['selector', 'text']
Expand Down Expand Up @@ -869,11 +870,13 @@ const TOOL_REGISTRY = [

withVisualSessionFields({
name: 'insert_text',
description: 'Insert text at the current cursor position via CDP Input.insertText. Bypasses DOM event dispatch and directly inserts into the focused element. When to use: for canvas-based editors (Excalidraw, Google Docs, Slack) where type_text does not work because there is no real input element. The element must already be focused or in edit mode (use double_click_at or click_at first). Related: type_text (for real DOM input fields), double_click_at (enter edit mode in canvas editors before inserting text), click_at (focus canvas element before inserting). Multi-agent: agent-scoped tabs; cross-agent reject with TAB_NOT_OWNED; cap configurable (default 8, 1-64). Pass tab_id only when this agent owns multiple tabs; auto-resolves otherwise.',
description: 'Insert text through CDP at the caret by default. Use position end to append or replace_all to replace an editable field; selector can identify that field. Canvas editors require focus first. Related: type_text, double_click_at, click_at. Multi-agent: agent-scoped tabs; cross-agent reject with TAB_NOT_OWNED; cap configurable (default 8, 1-64).',
inputSchema: {
type: 'object',
properties: {
text: { type: 'string', description: 'Text to insert at current cursor position via CDP' },
position: { type: 'string', enum: ['caret', 'end', 'replace_all'], description: 'Insertion position; defaults to caret.' },
selector: { type: 'string', description: 'Optional CSS selector identifying exactly one editable field.' },
tab_id: { type: 'number', description: 'Optional. Tab id this action targets. Omit when the calling agent owns exactly one tab; pass to disambiguate when the agent owns multiple. Single-tab agents and legacy popup/sidepanel/autopilot do not need to pass this.' }
},
required: ['text']
Expand Down Expand Up @@ -1459,6 +1462,13 @@ function getToolByName(name) {
return TOOL_REGISTRY.find(t => t.name === name) || null;
}

/** Resolve the public MCP name or the content/CDP verb sent on the bridge. */
function getToolByNameOrVerb(nameOrVerb) {
const name = typeof nameOrVerb === 'string' ? nameOrVerb.trim() : '';
if (!name) return null;
return getToolByName(name) || getToolByName(_iconVerbMap().get(name));
}

/**
* Get all read-only tools (those that bypass the mutation queue).
* @returns {ToolDefinition[]} Array of read-only tool definitions
Expand Down Expand Up @@ -1518,7 +1528,7 @@ function resolveIconActivity(nameOrVerb) {
if (!raw) return 'sweep';
const name = _iconVerbMap().get(raw) || raw;
if (name === 'invoke_capability') return null;
const def = getToolByName(name);
const def = getToolByNameOrVerb(raw);
if ((def && def._readOnly === true) || ICON_READ_ONLY_EXTRAS.has(name)) return 'orbit';
return 'sweep';
}
Expand All @@ -1533,6 +1543,7 @@ if (typeof module !== 'undefined' && module.exports) {
TOOL_REGISTRY,
resolveIconActivity,
getToolByName,
getToolByNameOrVerb,
getReadOnlyTools,
getToolsByRoute,
VISUAL_SESSION_FIELDS,
Expand Down
Loading
Loading