From 34e0fb4711f08d3d6519b48bb415aed8f4556d9e Mon Sep 17 00:00:00 2001 From: ValeroK Date: Mon, 12 May 2025 22:04:02 +0300 Subject: [PATCH] add option for a token autentication given by the user and ability to replace the port also update readme accordignly --- README.md | 109 ++++++++++++++++++++++++++++++++++++++++ manifest.json | 3 ++ options.html | 32 ++++++++++++ src/context.ts | 10 ++++ src/index.ts | 38 +++++++++----- src/server.ts | 54 +++++++++++++++++--- src/tools/background.ts | 46 +++++++++++++++++ src/tools/options.ts | 48 ++++++++++++++++++ src/ws.ts | 7 ++- tsconfig.json | 3 +- 10 files changed, 329 insertions(+), 21 deletions(-) create mode 100644 manifest.json create mode 100644 options.html create mode 100644 src/tools/background.ts create mode 100644 src/tools/options.ts diff --git a/README.md b/README.md index a85af20..78823e5 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,115 @@ Browser MCP is an MCP server + Chrome extension that allows you to automate your - 👤 Logged In: Uses your existing browser profile, keeping you logged into all your services. - 🥷🏼 Stealth: Avoids basic bot detection and CAPTCHAs by using your real browser fingerprint. +## Authentication + +Browser MCP now supports authentication using a user-generated token (up to 64 characters). This token must be set both on the server and in the browser extension. + +### Setting the Token + +- **On the server:** + - Pass the token as a CLI argument: `--token ` + - Or set the environment variable: `BROWSER_MCP_AUTH_TOKEN=` +- **In the browser extension:** + - After enabling the extension, you will be prompted to enter the same token. Enter the exact token you used for the server. + +### How it works +- The browser extension will send the token to the server when connecting. +- The server will only accept connections and requests from extensions that provide the correct token. +- If the token is missing or incorrect, the connection will be rejected. + +**Note:** +- The token can be any string up to 64 characters. It is recommended to use a strong, random value. +- Keep your token secret. Anyone with access to the token can control your browser via Browser MCP. + +## Setting Environment Variables for Chrome (Browser Extension) + +To allow the Browser MCP Chrome extension to read your authentication token, you may need to set an environment variable for Chrome. This is especially useful if the extension is configured to read the token from your environment. + +### On macOS/Linux: + +1. Open your terminal. +2. Start Chrome with the environment variable set: + + ```sh + BROWSER_MCP_AUTH_TOKEN= open -a "/Applications/Google Chrome.app" + ``` + Or, if you use `google-chrome` from the command line: + ```sh + BROWSER_MCP_AUTH_TOKEN= google-chrome + ``` + +### On Windows: + +1. Open Command Prompt (cmd.exe). +2. Run: + ```cmd + set BROWSER_MCP_AUTH_TOKEN= + start chrome + ``` + +> **Note:** +> Replace `` with your actual token. This ensures Chrome (and the extension) can access the token from the environment. + +## Example: MCP Client Configuration + +To use Browser MCP with an MCP-compatible client (such as Cursor, VS Code, or other tools), add the following to your MCP client configuration file (e.g., `.mcp.json` or `settings.json`): + +```json +{ + "mcpServers": { + "browsermcp": { + "command": "npx", + "args": ["@browsermcp/mcp@latest", "--token", ""] + } + } +} +``` + +- Replace `` with your chosen authentication token (up to 64 characters). +- This will ensure the MCP client starts the Browser MCP server with the correct token. + +## Extension Options: Token and Port Override + +The Browser MCP Chrome extension now includes an options page where you can: +- Set your authentication token (up to 64 characters). +- Set a port override (if you want to use a port other than the default 9234). + +To access the options page: +1. Go to `chrome://extensions/` in Chrome. +2. Find "Browser MCP" and click "Details". +3. Click "Extension options" or "Options". +4. Enter your token and (optionally) the port, then save. + +The extension will use these settings for all future connections. + +## Extension Capabilities + +The extension now reports its capabilities to the server and clients, including: +- Extension name and version +- Supported features: `token-auth`, `port-override`, `capabilities-report` + +You (or your client) can request these capabilities via a message to the extension, or they will be sent automatically after authentication. + +## Example: MCP Client Configuration (with Port Override) + +If you want to specify a custom port, update your MCP client configuration as follows: + +```json +{ + "mcpServers": { + "browsermcp": { + "command": "npx", + "args": ["@browsermcp/mcp@latest", "--token", "", "--port", ""] + } + } +} +``` +- Replace `` with your authentication token. +- Replace `` with your chosen port (if overriding the default). + +If you do not specify a port, the default (9234) will be used. + ## Contributing This repo contains all the core MCP code for Browser MCP, but currently cannot yet be built on its own due to dependencies on utils and types from the monorepo where it's developed. diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..3cedf52 --- /dev/null +++ b/manifest.json @@ -0,0 +1,3 @@ +{ + "options_page": "options.html" +} \ No newline at end of file diff --git a/options.html b/options.html new file mode 100644 index 0000000..d6848fe --- /dev/null +++ b/options.html @@ -0,0 +1,32 @@ + + + + + Browser MCP Extension Options + + + +

Browser MCP Extension Settings

+
+ + +
+ + +
+
+
+ + + \ No newline at end of file diff --git a/src/context.ts b/src/context.ts index 1e2fa3c..c4f3e4c 100644 --- a/src/context.ts +++ b/src/context.ts @@ -9,6 +9,7 @@ const noConnectionMessage = `No connection to browser extension. In order to pro export class Context { private _ws: WebSocket | undefined; + private _authenticated: boolean = false; get ws(): WebSocket { if (!this._ws) { @@ -25,6 +26,14 @@ export class Context { return !!this._ws; } + setAuthenticated(auth: boolean) { + this._authenticated = auth; + } + + isAuthenticated(): boolean { + return this._authenticated; + } + async sendSocketMessage>( type: T, payload: MessagePayload, @@ -48,5 +57,6 @@ export class Context { return; } await this._ws.close(); + this._authenticated = false; } } diff --git a/src/index.ts b/src/index.ts index 2278a75..5dd2b79 100644 --- a/src/index.ts +++ b/src/index.ts @@ -41,26 +41,38 @@ const snapshotTools: Tool[] = [ const resources: Resource[] = []; -async function createServer(): Promise { - return createServerWithTools({ - name: appConfig.name, - version: packageJSON.version, - tools: snapshotTools, - resources, - }); -} +let authToken: string | undefined = process.env.BROWSER_MCP_AUTH_TOKEN; -/** - * Note: Tools must be defined *before* calling `createServer` because only declarations are hoisted, not the initializations - */ program + .option('-t, --token ', 'Authentication token (up to 64 characters)') .version("Version " + packageJSON.version) .name(packageJSON.name) - .action(async () => { - const server = await createServer(); + .action(async (options) => { + if (options.token) { + if (options.token.length > 64) { + console.error('Token must be 64 characters or less.'); + process.exit(1); + } + authToken = options.token; + } + if (!authToken) { + console.error('Authentication token required. Set via --token or BROWSER_MCP_AUTH_TOKEN.'); + process.exit(1); + } + const server = await createServer(authToken); setupExitWatchdog(server); const transport = new StdioServerTransport(); await server.connect(transport); }); program.parse(process.argv); + +async function createServer(authToken: string): Promise { + return createServerWithTools({ + name: appConfig.name, + version: packageJSON.version, + tools: snapshotTools, + resources, + authToken, + }); +} diff --git a/src/server.ts b/src/server.ts index 7c118f0..debd966 100644 --- a/src/server.ts +++ b/src/server.ts @@ -16,10 +16,11 @@ type Options = { version: string; tools: Tool[]; resources: Resource[]; + authToken: string; }; export async function createServerWithTools(options: Options): Promise { - const { name, version, tools, resources } = options; + const { name, version, tools, resources, authToken } = options; const context = new Context(); const server = new Server( { name, version }, @@ -33,22 +34,61 @@ export async function createServerWithTools(options: Options): Promise { const wss = await createWebSocketServer(); wss.on("connection", (websocket) => { - // Close any existing connections - if (context.hasWs()) { - context.ws.close(); - } - context.ws = websocket; + let authenticated = false; + const authTimeout = setTimeout(() => { + if (!authenticated) { + websocket.close(4001, "Authentication required"); + } + }, 5000); + websocket.once("message", (data) => { + try { + const msg = JSON.parse(data.toString()); + if (typeof msg.token === "string" && msg.token === authToken) { + authenticated = true; + clearTimeout(authTimeout); + // Close any existing connections + if (context.hasWs()) { + context.ws.close(); + } + context.ws = websocket; + context.setAuthenticated(true); + } else { + websocket.close(4002, "Invalid authentication token"); + } + } catch { + websocket.close(4003, "Malformed authentication message"); + } + }); + websocket.on("close", () => { + context.setAuthenticated(false); + }); }); + function ensureAuthenticated() { + if (!context.isAuthenticated()) { + return { + content: [{ type: "text", text: "Not authenticated. Please connect with a valid token." }], + isError: true, + }; + } + return null; + } + server.setRequestHandler(ListToolsRequestSchema, async () => { + const err = ensureAuthenticated(); + if (err) return err; return { tools: tools.map((tool) => tool.schema) }; }); server.setRequestHandler(ListResourcesRequestSchema, async () => { + const err = ensureAuthenticated(); + if (err) return err; return { resources: resources.map((resource) => resource.schema) }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { + const err = ensureAuthenticated(); + if (err) return err; const tool = tools.find((tool) => tool.schema.name === request.params.name); if (!tool) { return { @@ -71,6 +111,8 @@ export async function createServerWithTools(options: Options): Promise { }); server.setRequestHandler(ReadResourceRequestSchema, async (request) => { + const err = ensureAuthenticated(); + if (err) return err; const resource = resources.find( (resource) => resource.schema.uri === request.params.uri, ); diff --git a/src/tools/background.ts b/src/tools/background.ts new file mode 100644 index 0000000..ae2b637 --- /dev/null +++ b/src/tools/background.ts @@ -0,0 +1,46 @@ +/// +// Utility to get token and port from storage +function getMcpSettings(callback: (settings: { token: string; port: number }) => void): void { + chrome.storage.local.get(['mcpToken', 'mcpPort'], (items: { [key: string]: any }) => { + callback({ + token: items.mcpToken || '', + port: items.mcpPort || 9234 // default port + }); + }); +} + +// Example: Use settings for WebSocket connection +function connectToMcpServer(): void { + getMcpSettings(({ token, port }) => { + const ws = new WebSocket(`ws://localhost:${port}`); + ws.onopen = () => { + ws.send(JSON.stringify({ token })); + // Optionally, send capabilities after auth + ws.send(JSON.stringify({ type: 'capabilities', data: getCapabilities() })); + }; + // ... handle other ws events ... + }); +} + +// Capabilities reporting +type Capabilities = { + extension: string; + version: string; + features: string[]; +}; + +function getCapabilities(): Capabilities { + return { + extension: 'browser-mcp', + version: chrome.runtime.getManifest().version, + features: ['token-auth', 'port-override', 'capabilities-report'] + }; +} + +// Listen for messages (optional, e.g., for getCapabilities) +chrome.runtime.onMessage.addListener((request: any, sender: chrome.runtime.MessageSender, sendResponse: (response?: any) => void) => { + if (request.type === 'getCapabilities') { + sendResponse(getCapabilities()); + } + // ... handle other messages ... +}); \ No newline at end of file diff --git a/src/tools/options.ts b/src/tools/options.ts new file mode 100644 index 0000000..8fe73b6 --- /dev/null +++ b/src/tools/options.ts @@ -0,0 +1,48 @@ +/// +// Saves options to chrome.storage.local +function saveOptions(e: Event): void { + e.preventDefault(); + const tokenInput = document.getElementById('token') as HTMLInputElement; + const portInput = document.getElementById('port') as HTMLInputElement; + const token = tokenInput.value.trim(); + const port = portInput.value.trim(); + if (token.length === 0 || token.length > 64) { + showStatus('Token must be between 1 and 64 characters.', true); + return; + } + chrome.storage.local.set({ + mcpToken: token, + mcpPort: port ? parseInt(port, 10) : null + }, () => { + showStatus('Options saved.'); + }); +} + +// Restores options from chrome.storage.local +function restoreOptions(): void { + chrome.storage.local.get(['mcpToken', 'mcpPort'], (items: { [key: string]: any }) => { + const tokenInput = document.getElementById('token') as HTMLInputElement; + const portInput = document.getElementById('port') as HTMLInputElement; + tokenInput.value = items.mcpToken || ''; + portInput.value = items.mcpPort || ''; + }); +} + +// Restore defaults +function restoreDefaults(): void { + chrome.storage.local.set({ mcpToken: '', mcpPort: null }, () => { + restoreOptions(); + showStatus('Defaults restored.'); + }); +} + +function showStatus(message: string, isError = false): void { + const status = document.getElementById('status') as HTMLElement; + status.textContent = message; + status.style.color = isError ? 'red' : 'green'; + setTimeout(() => { status.textContent = ''; }, 3000); +} + +document.getElementById('options-form')!.addEventListener('submit', saveOptions); +document.getElementById('restore')!.addEventListener('click', restoreDefaults); +document.addEventListener('DOMContentLoaded', restoreOptions); \ No newline at end of file diff --git a/src/ws.ts b/src/ws.ts index fb9f25a..b255845 100644 --- a/src/ws.ts +++ b/src/ws.ts @@ -7,11 +7,16 @@ import { isPortInUse, killProcessOnPort } from "@/utils/port"; export async function createWebSocketServer( port: number = mcpConfig.defaultWsPort, + onConnection?: (ws: import('ws').WebSocket) => void, ): Promise { killProcessOnPort(port); // Wait until the port is free while (await isPortInUse(port)) { await wait(100); } - return new WebSocketServer({ port }); + const wss = new WebSocketServer({ port }); + if (onConnection) { + wss.on('connection', onConnection); + } + return wss; } diff --git a/tsconfig.json b/tsconfig.json index d466d95..994b0a0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,8 @@ "rootDir": "src", "paths": { "@/*": ["./src/*"] - } + }, + "types": ["chrome"] }, "include": ["src/**/*.ts"], "exclude": ["node_modules"]