diff --git a/README.md b/README.md
index 23fc560..92cdcc6 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
-
+
diff --git a/extension/handlers/navigation.js b/extension/handlers/navigation.js
index dda1f43..7d9b69f 100644
--- a/extension/handlers/navigation.js
+++ b/extension/handlers/navigation.js
@@ -19,7 +19,8 @@ export async function getActiveTab() {
}
export async function handleNavigate(params, _sessionId, _agentName, signal) {
- const { url, waitUntil = 'load', tabId, snapshot: wantSnapshot = true } = params;
+ let { url } = params;
+ const { waitUntil = 'load', tabId, snapshot: wantSnapshot = true } = params;
// navigate is the one page tool allowed to omit tabId → active tab fallback.
const tab = tabId != null ? await resolveTab(tabId) : await getActiveTab();
@@ -27,8 +28,15 @@ export async function handleNavigate(params, _sessionId, _agentName, signal) {
// so `chrome.tabs.onUpdated` never fires `status === 'complete'` and the wait
// below would hang for the full 55s timeout. Detect this case and skip the
// wait entirely — the SPA router updates client-side near-instantly.
+ // "back"/"forward" walk the tab's history instead of being treated as a
+ // relative URL (which used to land on chrome-extension:///back).
+ const historyStep = url === 'back' || url === 'forward' ? url : null;
+ const go = () => historyStep === 'back' ? chrome.tabs.goBack(tab.id)
+ : historyStep === 'forward' ? chrome.tabs.goForward(tab.id)
+ : chrome.tabs.update(tab.id, { url });
+
const currentTab = await chrome.tabs.get(tab.id);
- const hashOnly = isHashOnlyChange(currentTab.url, url);
+ const hashOnly = !historyStep && isHashOnlyChange(currentTab.url, url);
// A promise that rejects when this call is cancelled (client gone / timeout
// forwarded from the bridge). Handlers that await long-running operations
@@ -84,7 +92,7 @@ export async function handleNavigate(params, _sessionId, _agentName, signal) {
};
chrome.tabs.onUpdated.addListener(listener);
if (wantDcl) pollTimer = setInterval(probeReady, 150);
- chrome.tabs.update(tab.id, { url }).catch((err) => {
+ go().catch((err) => {
chrome.tabs.onUpdated.removeListener(listener);
if (pollTimer) clearInterval(pollTimer);
reject(err);
@@ -111,6 +119,9 @@ export async function handleNavigate(params, _sessionId, _agentName, signal) {
// Return the snapshot inline ONLY if the caller asked for it (default true).
// Skipping it (snapshot:false) saves a large chunk of tokens when the agent
// intends to call browser_snapshot itself or doesn't need the tree yet.
+ // For back/forward report where we actually landed, not the keyword.
+ if (historyStep) url = (await chrome.tabs.get(tab.id)).url;
+
if (!wantSnapshot) {
return { url, status: 'navigated', tabId: tab.id };
}
diff --git a/extension/manifest.json b/extension/manifest.json
index 80c93f3..479f281 100644
--- a/extension/manifest.json
+++ b/extension/manifest.json
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Browser Controller",
- "version": "2.2.0",
+ "version": "2.2.1",
"description": "Let AI agents control your real browser - your tabs, your sessions, your logins",
"icons": {
"16": "icons/icon16.png",
diff --git a/mcp-server/src/tools/navigate.ts b/mcp-server/src/tools/navigate.ts
index 14bbecf..949ef03 100644
--- a/mcp-server/src/tools/navigate.ts
+++ b/mcp-server/src/tools/navigate.ts
@@ -6,7 +6,7 @@ export const navigateTool: ToolDefinition = {
name: 'browser_navigate',
summary: 'Navigate a tab to a URL (+ optional inline snapshot)', description: 'Navigate to a URL in a browser tab. If tabId is omitted, navigates the active tab (or use browser_tabs create to open a new one). By default also returns a compact snapshot of the new page so you can act immediately; set snapshot:false to skip it and save tokens.',
inputSchema: z.object({
- url: z.string().describe('The URL to navigate to'),
+ url: z.string().describe('The URL to navigate to, or "back" / "forward" to move through the tab history'),
tabId: optionalTabId(),
waitUntil: z
.enum(['load', 'domcontentloaded'])
diff --git a/package-lock.json b/package-lock.json
index d116ab2..39fee35 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "browser-controller",
- "version": "2.2.0",
+ "version": "2.2.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "browser-controller",
- "version": "2.2.0",
+ "version": "2.2.1",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.30.0",
diff --git a/package.json b/package.json
index 0976c4c..014e6b1 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "browser-controller",
"mcpName": "io.github.noiemany/browser-controller",
- "version": "2.2.0",
+ "version": "2.2.1",
"description": "MCP server + Chrome extension that gives AI agents control of your real browser with existing sessions and logins",
"type": "module",
"bin": {
diff --git a/scripts/drive.mjs b/scripts/drive.mjs
index 421722b..7f051c8 100644
--- a/scripts/drive.mjs
+++ b/scripts/drive.mjs
@@ -13,7 +13,10 @@ import path from 'node:path';
const STATE = path.join(os.homedir(), '.browser-controller');
const token = JSON.parse(fs.readFileSync(path.join(STATE, 'token.json'), 'utf8')).token;
-const sockPath = path.join(STATE, 'daemon.sock');
+// Must match daemon-config.ts: Windows uses a named pipe, not a socket file.
+const sockPath = process.platform === 'win32'
+ ? '\\\\.\\pipe\\browser-controller'
+ : path.join(STATE, 'daemon.sock');
const tool = process.argv[2];
if (!tool) { console.error('usage: drive.mjs [json params]'); process.exit(1); }
diff --git a/server.json b/server.json
index 6240a00..660b249 100644
--- a/server.json
+++ b/server.json
@@ -6,12 +6,12 @@
"url": "https://github.com/compnew2006/browser-controller",
"source": "github"
},
- "version": "2.2.0",
+ "version": "2.2.1",
"packages": [
{
"registryType": "npm",
"identifier": "browser-controller",
- "version": "2.2.0",
+ "version": "2.2.1",
"transport": {
"type": "stdio"
}