Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/firefox/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export class FirefoxClient {
this.core = new FirefoxCore(options);
}

private getBidi(): BiDiFacade {
getBidi(): BiDiFacade {
if (!this.bidi) {
throw new Error('Not connected');
}
Expand Down
271 changes: 105 additions & 166 deletions src/tools/firefox-prefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,36 @@
* Requires MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1
*/

import type { BrowsingContext } from 'webdriver-bidi-protocol';
import { logError } from '../utils/logger.js';
import { successResponse } from '../utils/response-helpers.js';
import { FirefoxClient } from '../firefox/index.js';
import { generatePrefScript } from '../firefox/pref-utils.js';
import { defineModule, defineToolHandler, type ToolDefinition } from './module.js';
import type { McpToolResponse } from '../types/common.js';

async function getPrivilegedContext(firefox: FirefoxClient) {
let contexts: BrowsingContext.InfoList = [];
try {
// Get privileged ("chrome") contexts
const result = await firefox.sendBiDiCommand('browsingContext.getTree', {
'moz:scope': 'chrome',
maxDepth: 1,
});
contexts = result.contexts || [];
} catch (error) {
logError('Failed to get privileged contexts', error);
}

if (contexts.length === 0) {
throw new Error(
'No privileged contexts available. Ensure MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 is set.'
);
}

return contexts[0]!.context;
}

// ============================================================================
// Tool: set_firefox_prefs
// ============================================================================
Expand Down Expand Up @@ -38,88 +63,45 @@ export const setFirefoxPrefsTool = {

export const handleSetFirefoxPrefs = defineToolHandler(
async (args: unknown): Promise<McpToolResponse> => {
try {
const { prefs } = args as { prefs: Record<string, string | number | boolean> };

if (!prefs || typeof prefs !== 'object') {
throw new Error('prefs parameter is required and must be an object');
}
const { prefs } = args as { prefs: Record<string, string | number | boolean> };

const prefEntries = Object.entries(prefs);
if (prefEntries.length === 0) {
return successResponse('No preferences to set');
}

const { getFirefox } = await import('../index.js');
const firefox = await getFirefox();

// Get privileged ("chrome") contexts
const result = await firefox.sendBiDiCommand('browsingContext.getTree', {
'moz:scope': 'chrome',
});

const contexts = result.contexts || [];
if (contexts.length === 0) {
throw new Error(
'No privileged contexts available. Ensure MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 is set.'
);
}
if (!prefs || typeof prefs !== 'object') {
throw new Error('prefs parameter is required and must be an object');
}

const driver = firefox.getDriver();
const chromeContextId = contexts[0]!.context;
const prefEntries = Object.entries(prefs);
if (prefEntries.length === 0) {
return successResponse('No preferences to set');
}

// Remember current context
const originalContextId = firefox.getCurrentContextId();
const { getFirefox } = await import('../index.js');
const firefox = await getFirefox();
const chromeContextId = await getPrivilegedContext(firefox);
const results: string[] = [];
const errors: string[] = [];

// Set each preference
for (const [name, value] of prefEntries) {
try {
// Switch to chrome context
await driver.switchTo().window(chromeContextId);
await driver.setContext('chrome');

const results: string[] = [];
const errors: string[] = [];

// Set each preference
for (const [name, value] of prefEntries) {
try {
const script = generatePrefScript(name, value);
await driver.executeScript(script);
results.push(` ${name} = ${JSON.stringify(value)}`);
} catch (error) {
errors.push(` ${name}: ${error instanceof Error ? error.message : String(error)}`);
}
}

const output: string[] = [];
if (results.length > 0) {
output.push(`Set ${results.length} preference(s):`);
output.push(...results);
}
if (errors.length > 0) {
output.push(`\nFailed to set ${errors.length} preference(s):`);
output.push(...errors);
}

return successResponse(output.join('\n'));
} finally {
// Restore previous context (skip if already on the right chrome context)
try {
if (originalContextId && originalContextId !== chromeContextId) {
await driver.setContext('content');
await driver.switchTo().window(originalContextId);
}
} catch {
// Ignore errors restoring context
}
}
} catch (error) {
if (error instanceof Error && error.message.includes('UnsupportedOperationError')) {
throw new Error(
'Chrome context access not enabled. Set MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 environment variable and restart Firefox.'
);
const script = generatePrefScript(name, value);
await firefox.getBidi().evaluate(chromeContextId, script);
results.push(` ${name} = ${JSON.stringify(value)}`);
} catch (error) {
errors.push(` ${name}: ${error instanceof Error ? error.message : String(error)}`);
}
throw error;
}

const output: string[] = [];
if (results.length > 0) {
output.push(`Set ${results.length} preference(s):`);
output.push(...results);
}
if (errors.length > 0) {
output.push(`\nFailed to set ${errors.length} preference(s):`);
output.push(...errors);
}

return successResponse(output.join('\n'));
}
);

Expand Down Expand Up @@ -149,105 +131,62 @@ export const getFirefoxPrefsTool = {

export const handleGetFirefoxPrefs = defineToolHandler(
async (args: unknown): Promise<McpToolResponse> => {
try {
const { names } = args as { names: string[] };

if (!names || !Array.isArray(names) || names.length === 0) {
throw new Error('names parameter is required and must be a non-empty array');
}

const { getFirefox } = await import('../index.js');
const firefox = await getFirefox();
const { names } = args as { names: string[] };

// Get privileged ("chrome") contexts
const result = await firefox.sendBiDiCommand('browsingContext.getTree', {
'moz:scope': 'chrome',
});

const contexts = result.contexts || [];
if (contexts.length === 0) {
throw new Error(
'No privileged contexts available. Ensure MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 is set.'
);
}

const driver = firefox.getDriver();
const chromeContextId = contexts[0]!.context;
if (!names || !Array.isArray(names) || names.length === 0) {
throw new Error('names parameter is required and must be a non-empty array');
}

// Remember current context
const originalContextId = firefox.getCurrentContextId();
const { getFirefox } = await import('../index.js');
const firefox = await getFirefox();
const chromeContextId = await getPrivilegedContext(firefox);
const results: string[] = [];
const errors: string[] = [];

// Read each preference
for (const name of names) {
try {
// Switch to chrome context
await driver.switchTo().window(chromeContextId);
await driver.setContext('chrome');

const results: string[] = [];
const errors: string[] = [];

// Read each preference
for (const name of names) {
try {
// Use getPrefType to determine how to read the pref
const script = `
(function() {
const type = Services.prefs.getPrefType(${JSON.stringify(name)});
if (type === Services.prefs.PREF_INVALID) {
return { exists: false };
} else if (type === Services.prefs.PREF_BOOL) {
return { exists: true, value: Services.prefs.getBoolPref(${JSON.stringify(name)}) };
} else if (type === Services.prefs.PREF_INT) {
return { exists: true, value: Services.prefs.getIntPref(${JSON.stringify(name)}) };
} else {
return { exists: true, value: Services.prefs.getStringPref(${JSON.stringify(name)}) };
}
})()
`;
const prefResult = (await driver.executeScript(`return ${script}`)) as {
exists: boolean;
value?: unknown;
};

if (prefResult.exists) {
results.push(` ${name} = ${JSON.stringify(prefResult.value)}`);
} else {
results.push(` ${name} = (not set)`);
}
} catch (error) {
errors.push(` ${name}: ${error instanceof Error ? error.message : String(error)}`);
}
}

const output: string[] = [];
if (results.length > 0) {
output.push(`Firefox Preferences:`);
output.push(...results);
}
if (errors.length > 0) {
output.push(`\nFailed to read ${errors.length} preference(s):`);
output.push(...errors);
}

return successResponse(output.join('\n'));
} finally {
// Restore previous context (skip if already on the right chrome context)
try {
if (originalContextId && originalContextId !== chromeContextId) {
await driver.setContext('content');
await driver.switchTo().window(originalContextId);
// Use getPrefType to determine how to read the pref
const script = `
(function() {
const type = Services.prefs.getPrefType(${JSON.stringify(name)});
if (type === Services.prefs.PREF_INVALID) {
return { exists: false };
} else if (type === Services.prefs.PREF_BOOL) {
return { exists: true, value: Services.prefs.getBoolPref(${JSON.stringify(name)}) };
} else if (type === Services.prefs.PREF_INT) {
return { exists: true, value: Services.prefs.getIntPref(${JSON.stringify(name)}) };
} else {
return { exists: true, value: Services.prefs.getStringPref(${JSON.stringify(name)}) };
}
} catch {
// Ignore errors restoring context
})()
`;
const prefResult = await firefox.getBidi().evaluate<{
exists: boolean;
value?: unknown;
}>(chromeContextId, script);

if (prefResult.exists) {
results.push(` ${name} = ${JSON.stringify(prefResult.value)}`);
} else {
results.push(` ${name} = (not set)`);
}
} catch (error) {
errors.push(` ${name}: ${error instanceof Error ? error.message : String(error)}`);
}
} catch (error) {
if (error instanceof Error && error.message.includes('UnsupportedOperationError')) {
throw new Error(
'Chrome context access not enabled. Set MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 environment variable and restart Firefox.'
);
}
throw error;
}

const output: string[] = [];
if (results.length > 0) {
output.push(`Firefox Preferences:`);
output.push(...results);
}
if (errors.length > 0) {
output.push(`\nFailed to read ${errors.length} preference(s):`);
output.push(...errors);
}

return successResponse(output.join('\n'));
}
);

Expand Down
Loading
Loading