Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,6 @@ dist
web-ext-artifacts/

.DS_Store

# Coding agent related files
task.md
8 changes: 7 additions & 1 deletion common/extension-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ export interface TabGroupCreatedExtensionMessage extends ExtensionMessageBase {
groupId: number;
}

export interface BookmarksExtensionMessage extends ExtensionMessageBase {
resource: "bookmarks";
bookmarksText: string;
}

export type ExtensionMessage =
| TabContentExtensionMessage
| TabsExtensionMessage
Expand All @@ -68,7 +73,8 @@ export type ExtensionMessage =
| ReorderedTabsExtensionMessage
| FindHighlightExtensionMessage
| TabsClosedExtensionMessage
| TabGroupCreatedExtensionMessage;
| TabGroupCreatedExtensionMessage
| BookmarksExtensionMessage;

export interface ExtensionError {
correlationId: string;
Expand Down
8 changes: 7 additions & 1 deletion common/server-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ export interface GroupTabsServerMessage extends ServerMessageBase {
groupTitle: string;
}

export interface GetBookmarksServerMessage extends ServerMessageBase {
cmd: "get-bookmarks";
query?: string;
}

export type ServerMessage =
| OpenTabServerMessage
| CloseTabsServerMessage
Expand All @@ -54,6 +59,7 @@ export type ServerMessage =
| GetTabContentServerMessage
| ReorderTabsServerMessage
| FindHighlightServerMessage
| GroupTabsServerMessage;
| GroupTabsServerMessage
| GetBookmarksServerMessage;

export type ServerMessageRequest = ServerMessage & { correlationId: string };
90 changes: 90 additions & 0 deletions firefox-extension/__tests__/message-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ describe("MessageHandler", () => {
"get-tab-web-content": true,
"reorder-browser-tabs": true,
"find-highlight-in-browser-tab": true,
"get-browser-bookmarks": true,
},
domainDenyList: [],
ports: [8089],
Expand All @@ -65,6 +66,7 @@ describe("MessageHandler", () => {
"get-tab-web-content": true,
"reorder-browser-tabs": true,
"find-highlight-in-browser-tab": true,
"get-browser-bookmarks": true,
},
domainDenyList: [],
ports: [8089],
Expand Down Expand Up @@ -139,6 +141,7 @@ describe("MessageHandler", () => {
"get-tab-web-content": true,
"reorder-browser-tabs": true,
"find-highlight-in-browser-tab": true,
"get-browser-bookmarks": true,
},
domainDenyList: ["example.com", "another.com"],
ports: [8089],
Expand Down Expand Up @@ -173,6 +176,7 @@ describe("MessageHandler", () => {
"get-tab-web-content": true,
"reorder-browser-tabs": true,
"find-highlight-in-browser-tab": true,
"get-browser-bookmarks": true,
},
domainDenyList: ["example.com", "another.com"],
ports: [8089],
Expand Down Expand Up @@ -395,6 +399,7 @@ describe("MessageHandler", () => {
"get-tab-web-content": true,
"reorder-browser-tabs": true,
"find-highlight-in-browser-tab": true,
"get-browser-bookmarks": true,
},
domainDenyList: ["example.com"], // Add example.com to deny list
ports: [8089],
Expand Down Expand Up @@ -549,5 +554,90 @@ describe("MessageHandler", () => {
expect(browser.find.find).not.toHaveBeenCalled();
});
});

describe("get-bookmarks command", () => {
it("should get bookmarks and send formatted text to the server", async () => {
// Arrange
const request: ServerMessageRequest = {
cmd: "get-bookmarks",
correlationId: "test-correlation-id",
};

const mockBookmarks = [
{
id: "1",
title: "Root",
type: "folder",
children: [
{
id: "2",
title: "Work",
type: "folder",
children: [
{
id: "3",
title: "GitHub",
type: "bookmark",
url: "https://github.com",
},
],
},
{
id: "4",
title: "Google",
type: "bookmark",
url: "https://google.com",
},
],
},
];
(browser.bookmarks.getTree as jest.Mock).mockResolvedValue(
mockBookmarks
);

// Act
await messageHandler.handleDecodedMessage(request);

// Assert
expect(browser.bookmarks.getTree).toHaveBeenCalled();
expect(mockClient.sendResourceToServer).toHaveBeenCalledWith({
resource: "bookmarks",
correlationId: "test-correlation-id",
bookmarksText: expect.stringContaining("[Folder] Root"),
});
});

it("should search bookmarks when query is provided", async () => {
// Arrange
const request: ServerMessageRequest = {
cmd: "get-bookmarks",
query: "GitHub",
correlationId: "test-correlation-id",
};

const mockSearchResults = [
{
id: "3",
title: "GitHub",
type: "bookmark",
url: "https://github.com",
},
];
(browser.bookmarks.search as jest.Mock).mockResolvedValue(
mockSearchResults
);

// Act
await messageHandler.handleDecodedMessage(request);

// Assert
expect(browser.bookmarks.search).toHaveBeenCalledWith("GitHub");
expect(mockClient.sendResourceToServer).toHaveBeenCalledWith({
resource: "bookmarks",
correlationId: "test-correlation-id",
bookmarksText: expect.stringContaining("- GitHub - https://github.com"),
});
});
});
});
});
4 changes: 4 additions & 0 deletions firefox-extension/__tests__/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ const mockBrowser = {
find: jest.fn(),
highlightResults: jest.fn(),
},
bookmarks: {
getTree: jest.fn(),
search: jest.fn(),
},
storage: {
local: {
get: jest.fn(),
Expand Down
6 changes: 6 additions & 0 deletions firefox-extension/extension-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ export const AVAILABLE_TOOLS: ToolInfo[] = [
id: "find-highlight-in-browser-tab",
name: "Find and Highlight in Browser Tab",
description: "Allows the MCP server to search for and highlight text in web pages"
},
{
id: "get-browser-bookmarks",
name: "Get Browser Bookmarks",
description: "Allows the MCP server to read your browser bookmarks"
}
];

Expand All @@ -62,6 +67,7 @@ export const COMMAND_TO_TOOL_ID: Record<ServerMessageRequest["cmd"], string> = {
"reorder-tabs": "reorder-browser-tabs",
"find-highlight": "find-highlight-in-browser-tab",
"group-tabs": "reorder-browser-tabs",
"get-bookmarks": "get-browser-bookmarks",
};

// Storage schema for tool settings
Expand Down
5 changes: 3 additions & 2 deletions firefox-extension/manifest.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
{
"manifest_version": 2,
"name": "Browser Control MCP",
"version": "1.5.0",
"version": "1.6.0",
"description": "An extension that allows a local MCP server to perform actions on the browser.",
"permissions": [
"tabs",
"tabGroups",
"history",
"storage"
"storage",
"bookmarks"
],
"optional_permissions": [
"*://*/*",
Expand Down
30 changes: 30 additions & 0 deletions firefox-extension/message-handler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ServerMessageRequest } from "@browser-control-mcp/common";
import { WebsocketClient } from "./client";
import { isCommandAllowed, isDomainInDenyList, COMMAND_TO_TOOL_ID, addAuditLogEntry } from "./extension-config";
import { formatBookmarksAsText } from "./util";

export class MessageHandler {
private client: WebsocketClient;
Expand Down Expand Up @@ -54,6 +55,9 @@ export class MessageHandler {
req.groupTitle
);
break;
case "get-bookmarks":
await this.sendBookmarks(req.correlationId, req.query);
break;
default:
const _exhaustiveCheck: never = req;
console.error("Invalid message received:", req);
Expand Down Expand Up @@ -325,4 +329,30 @@ export class MessageHandler {
groupId: tabGroup.id,
});
}

private async sendBookmarks(
correlationId: string,
query?: string
): Promise<void> {
// Get the bookmarks tree
let bookmarks: browser.bookmarks.BookmarkTreeNode[];

if (query) {
// If query is provided, search bookmarks
bookmarks = await browser.bookmarks.search(query);
} else {
// Get all bookmarks from the root
bookmarks = await browser.bookmarks.getTree();
}

// Convert bookmarks tree to hierarchical text
const bookmarksText = formatBookmarksAsText(bookmarks, query ? false : true);

await this.client.sendResourceToServer({
resource: "bookmarks",
correlationId,
bookmarksText,
});
}

}
4 changes: 2 additions & 2 deletions firefox-extension/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion firefox-extension/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "firefox-extension",
"version": "1.5.0",
"version": "1.6.0",
"main": "dist/background.js",
"scripts": {
"build": "esbuild background.ts --bundle --outfile=dist/background.js && esbuild options.ts --bundle --outfile=dist/options.js",
Expand Down
41 changes: 41 additions & 0 deletions firefox-extension/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@

export function formatBookmarksAsText(
bookmarks: browser.bookmarks.BookmarkTreeNode[],
isTree: boolean,
level: number = 0,
parentPrefix: string = ""
): string {
let result = "";
let counter = 1;

for (const bookmark of bookmarks) {
const indent = " ".repeat(level);
let prefix: string;

if (isTree) {
// Create hierarchical numbering like 1., 1.1, 1.2, 2., 2.1, etc.
const currentNumber = level === 0 ? `${counter}` : `${parentPrefix}.${counter}`;
prefix = `${currentNumber}.`;
} else {
prefix = "-";
}

if (bookmark.type === "folder") {
result += `${indent}${prefix} [Folder] ${bookmark.title}\n`;
if (bookmark.children && bookmark.children.length > 0) {
const childPrefix = level === 0 ? `${counter}` : `${parentPrefix}.${counter}`;
result += formatBookmarksAsText(bookmark.children, isTree, level + 1, childPrefix);
}
} else if (bookmark.type === "bookmark") {
result += `${indent}${prefix} ${bookmark.title}`;
if (bookmark.url) {
result += ` - ${bookmark.url}`;
}
result += "\n";
}

counter++;
}

return result;
}
9 changes: 9 additions & 0 deletions mcp-server/browser-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,15 @@ export class BrowserAPI {
return message.groupId;
}

async getBookmarks(query?: string): Promise<string> {
const correlationId = this.sendMessageToExtension({
cmd: "get-bookmarks",
query,
});
const message = await this.waitForResponse(correlationId, "bookmarks");
return message.bookmarksText;
}

private createSignature(payload: string): string {
if (!this.sharedSecret) {
throw new Error("Shared secret not initialized");
Expand Down
6 changes: 5 additions & 1 deletion mcp-server/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"dxt_version": "0.1",
"name": "browser-control-firefox",
"version": "1.5.1",
"version": "1.6.0",
"display_name": "Firefox Control",
"description": "Control Mozilla Firefox: tabs, history and web content (privacy aware)",
"long_description": "This MCP server provides AI assistants with access to the browser's tab management, browsing history, page search, and webpage text content through a local MCP (Model Context Protocol) connection.\n\nBefore accessing any webpage content, explicit user consent is required for each domain through browser-side permissions.\n\n**Note:** This extension requires a separate Firefox add-on component, which is available at [https://addons.mozilla.org/en-US/firefox/addon/browser-control-mcp/](https://addons.mozilla.org/en-US/firefox/addon/browser-control-mcp/). Start by installing the Firefox add-on, then copy the secret key from the add-on settings page (it will open automatically) and paste it into this extension's configuration.",
Expand Down Expand Up @@ -82,6 +82,10 @@
{
"name": "find-highlight-in-browser-tab",
"description": "Find and highlight text in a browser tab"
},
{
"name": "get-browser-bookmarks",
"description": "Get the user's browser bookmarks as a hierarchical text structure"
}
]
}
4 changes: 2 additions & 2 deletions mcp-server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading