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
12 changes: 9 additions & 3 deletions docs/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# Tool reference

The server exposes 54 tools grouped into 16 modules. Which modules are
The server exposes 55 tools grouped into 16 modules. Which modules are
enabled depends on `--tool-preset` or `--tools`; see
[Tool modules and presets](../README.md#tool-modules-and-presets) in the README.

Expand All @@ -21,7 +21,7 @@ Mozilla-internal build and `MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1`; the public packag
| `screenshot` | 2 | yes | yes | yes | yes | yes |
| `downloads` | 3 | - | yes | yes | yes | yes |
| `utilities` | 4 | - | yes | yes | yes | yes |
| `management` | 3 | - | yes | yes | yes | yes |
| `management` | 4 | - | yes | yes | yes | yes |
| `webextension` | 2 | - | yes | yes | yes | yes |
| `profiler` | 3 | - | - | yes | yes | yes |
| `screencast` | 2 | - | yes | yes | yes | yes |
Expand Down Expand Up @@ -391,7 +391,7 @@ Parameters:

## management

Inspect Firefox info/output and restart the browser.
Inspect Firefox options and logs, restart and close the browser.

### `get_firefox_output`

Expand Down Expand Up @@ -426,6 +426,12 @@ Parameters:
- `startUrl` (string, optional) - URL to navigate to after restart (optional, uses about:blank if not specified)
- `prefs` (object, optional) - Firefox preferences to set at startup. Values are auto-typed: true/false become booleans, integers become numbers, everything else is a string. Requires MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1.

### `close_firefox_session`

Ends the browser session. If the server connected to your existing Firefox, this releases the connection and leaves Firefox running. If the server started Firefox itself, this closes it. Call this when the browser task is complete and no further browser interaction is expected.

No parameters.

## webextension

Install and uninstall web extensions.
Expand Down
41 changes: 40 additions & 1 deletion src/tools/firefox-management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,12 +380,51 @@ export const handleRestartFirefox = defineToolHandler(async (input: unknown) =>
}
});

// ============================================================================
// Tool: close_firefox_session
// ============================================================================

export const closeFirefoxSessionTool = {
name: 'close_firefox_session',
description:
'Ends the browser session. If the server connected to your existing Firefox, ' +
'this releases the connection and leaves Firefox running. If the server started Firefox itself, this closes it. ' +
'Call this when the browser task is complete and no further browser interaction is expected.',
annotations: {
readOnlyHint: false,
},
inputSchema: {
type: 'object',
properties: {},
},
} satisfies ToolDefinition;

export const handleCloseFirefoxSession = defineToolHandler(async (_args: unknown) => {
const { getFirefoxIfRunning, resetFirefox } = await import('../index.js');

const currentFirefox = getFirefoxIfRunning();
if (!currentFirefox) {
return successResponse('No Firefox session is currently active.');
}

// Read options before calling resetFirefox().
const { connectExisting } = currentFirefox.getOptions();
await resetFirefox();

return successResponse(
connectExisting
? 'Disconnected from Firefox. The browser is still running.'
: 'Closed the Firefox instance started by this server, a new session will start if you use browser tools again.'
);
});

export const module = defineModule({
name: 'management',
description: 'Inspect Firefox info/output and restart the browser.',
description: 'Inspect Firefox options and logs, restart and close the browser.',
tools: [
[getFirefoxLogsTool, handleGetFirefoxLogs],
[getFirefoxInfoTool, handleGetFirefoxInfo],
[restartFirefoxTool, handleRestartFirefox],
[closeFirefoxSessionTool, handleCloseFirefoxSession],
],
});
77 changes: 76 additions & 1 deletion tests/tools/firefox-management.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Unit tests for Firefox management tools (restart_firefox, get_firefox_info, get_firefox_output)
* Unit tests for Firefox management tools (close_firefox_session, restart_firefox, get_firefox_info, get_firefox_output)
*/

import { describe, it, expect, vi, beforeEach } from 'vitest';
Expand All @@ -25,6 +25,81 @@ vi.mock('../../src/index.js', () => ({
}));

describe('Firefox Management Tools', () => {
describe('closeFirefoxSession', () => {
beforeEach(() => {
vi.clearAllMocks();
mockArgs.firefoxPath = undefined;
mockArgs.profilePath = undefined;
});

describe('when Firefox is NOT running', () => {
beforeEach(() => {
mockGetFirefoxIfRunning.mockReturnValue(null);
});

it('should respond when firefox session was not found', async () => {
const { handleCloseFirefoxSession } = await import('../../src/tools/firefox-management.js');

const result = await handleCloseFirefoxSession({});

// Make sure only the getFirefoxIfRunning variant was called
expect(mockGetFirefoxIfRunning).toHaveBeenCalled();
expect(mockGetFirefox).not.toHaveBeenCalled();

expect(mockResetFirefox).not.toHaveBeenCalled();
expect(result.content[0].text).toContain('No Firefox session is currently active.');
});
});

describe('when Firefox is running', () => {
const mockFirefoxInstance = {
getOptions: vi.fn(),
ensureConnected: vi.fn(),
close: vi.fn(),
};

beforeEach(() => {
mockGetFirefoxIfRunning.mockReturnValue(mockFirefoxInstance);
});

it('should reset firefox when connect-existing=false', async () => {
mockFirefoxInstance.getOptions.mockReturnValue({
connectExisting: false,
});

const { handleCloseFirefoxSession } = await import('../../src/tools/firefox-management.js');
const result = await handleCloseFirefoxSession({});

// Make sure only the getFirefoxIfRunning variant was called
expect(mockGetFirefoxIfRunning).toHaveBeenCalled();
expect(mockGetFirefox).not.toHaveBeenCalled();

expect(mockResetFirefox).toHaveBeenCalled();
expect(result.content[0].text).toContain(
'Closed the Firefox instance started by this server, a new session will start if you use browser tools again.'
);
});

it('should reset firefox when connect-existing=true', async () => {
mockFirefoxInstance.getOptions.mockReturnValue({
connectExisting: true,
});

const { handleCloseFirefoxSession } = await import('../../src/tools/firefox-management.js');
const result = await handleCloseFirefoxSession({});

// Make sure only the getFirefoxIfRunning variant was called
expect(mockGetFirefoxIfRunning).toHaveBeenCalled();
expect(mockGetFirefox).not.toHaveBeenCalled();

expect(mockResetFirefox).toHaveBeenCalled();
expect(result.content[0].text).toContain(
'Disconnected from Firefox. The browser is still running.'
);
});
});
});

describe('restartFirefoxTool schema', () => {
it('should have profilePath in input schema properties', () => {
const { properties } = restartFirefoxTool.inputSchema as {
Expand Down