diff --git a/docs/tools.md b/docs/tools.md index 91e7bdb..8d071ea 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -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. @@ -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 | @@ -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` @@ -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. diff --git a/src/tools/firefox-management.ts b/src/tools/firefox-management.ts index f363a37..8810c31 100644 --- a/src/tools/firefox-management.ts +++ b/src/tools/firefox-management.ts @@ -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], ], }); diff --git a/tests/tools/firefox-management.test.ts b/tests/tools/firefox-management.test.ts index 65ac173..72eb628 100644 --- a/tests/tools/firefox-management.test.ts +++ b/tests/tools/firefox-management.test.ts @@ -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'; @@ -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 {