From a17cd71bb6ea6b285e7b5f0cd12e627624522acc Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Mon, 19 Jan 2026 12:35:02 +0000 Subject: [PATCH] Implement uninstall_extension tool --- scripts/generate-docs.ts | 5 ----- src/McpContext.ts | 4 ++++ src/tools/ToolDefinition.ts | 1 + src/tools/extensions.ts | 21 +++++++++++++++++++++ tests/index.test.ts | 12 +----------- tests/tools/extensions.test.ts | 32 ++++++++++++++++++++++++++++++-- 6 files changed, 57 insertions(+), 18 deletions(-) diff --git a/scripts/generate-docs.ts b/scripts/generate-docs.ts index 4cfd316cc..8ae3dc191 100644 --- a/scripts/generate-docs.ts +++ b/scripts/generate-docs.ts @@ -278,11 +278,6 @@ async function generateToolDocumentation(): Promise { // Convert ToolDefinitions to ToolWithAnnotations const toolsWithAnnotations: ToolWithAnnotations[] = tools .filter(tool => { - // Filter out extension tools - if (tool.name === 'install_extension') { - return false; - } - if (!tool.annotations.conditions) { return true; } diff --git a/src/McpContext.ts b/src/McpContext.ts index 05bf6f16f..602a41ac3 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -694,4 +694,8 @@ export class McpContext implements Context { async installExtension(path: string): Promise { return this.browser.installExtension(path); } + + async uninstallExtension(id: string): Promise { + return this.browser.uninstallExtension(id); + } } diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index ddb3d5f7d..73bc18d18 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -125,6 +125,7 @@ export type Context = Readonly<{ */ resolveCdpElementId(cdpBackendNodeId: number): string | undefined; installExtension(path: string): Promise; + uninstallExtension(id: string): Promise; }>; export function defineTool( diff --git a/src/tools/extensions.ts b/src/tools/extensions.ts index 245c25b7a..42ca85f43 100644 --- a/src/tools/extensions.ts +++ b/src/tools/extensions.ts @@ -9,12 +9,15 @@ import {zod} from '../third_party/index.js'; import {ToolCategory} from './categories.js'; import {defineTool} from './ToolDefinition.js'; +const EXTENSIONS_CONDITION = 'experimentalExtensionSupport'; + export const installExtension = defineTool({ name: 'install_extension', description: 'Installs a Chrome extension from the given path.', annotations: { category: ToolCategory.EXTENSIONS, readOnlyHint: false, + conditions: [EXTENSIONS_CONDITION], }, schema: { path: zod @@ -27,3 +30,21 @@ export const installExtension = defineTool({ response.appendResponseLine(`Extension installed. Id: ${id}`); }, }); + +export const uninstallExtension = defineTool({ + name: 'uninstall_extension', + description: 'Uninstalls a Chrome extension by its ID.', + annotations: { + category: ToolCategory.EXTENSIONS, + readOnlyHint: false, + conditions: [EXTENSIONS_CONDITION], + }, + schema: { + id: zod.string().describe('ID of the extension to uninstall.'), + }, + handler: async (request, response, context) => { + const {id} = request.params; + await context.uninstallExtension(id); + response.appendResponseLine(`Extension uninstalled. Id: ${id}`); + }, +}); diff --git a/tests/index.test.ts b/tests/index.test.ts index 0490af334..5b3f7a54e 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -98,17 +98,7 @@ describe('e2e', () => { const fileTools = await import(`../src/tools/${file}`); for (const maybeTool of Object.values(fileTools)) { if ('name' in maybeTool) { - if (maybeTool.annotations?.conditions?.includes('computerVision')) { - continue; - } - if ( - maybeTool.annotations?.conditions?.includes( - 'experimentalInteropTools', - ) - ) { - continue; - } - if (maybeTool.name === 'install_extension') { + if (maybeTool.annotations?.conditions) { continue; } definedNames.push(maybeTool.name); diff --git a/tests/tools/extensions.test.ts b/tests/tools/extensions.test.ts index f1ed6566f..effaf4807 100644 --- a/tests/tools/extensions.test.ts +++ b/tests/tools/extensions.test.ts @@ -8,7 +8,10 @@ import assert from 'node:assert'; import path from 'node:path'; import {describe, it} from 'node:test'; -import {installExtension} from '../../src/tools/extensions.js'; +import { + installExtension, + uninstallExtension, +} from '../../src/tools/extensions.js'; import {withMcpContext} from '../utils.js'; const EXTENSION_PATH = path.join( @@ -17,8 +20,9 @@ const EXTENSION_PATH = path.join( ); describe('extension', () => { - it('installs an extension and verifies it is listed in chrome://extensions', async () => { + it('installs and uninstalls an extension and verifies it in chrome://extensions', async () => { await withMcpContext(async (response, context) => { + // Install the extension await installExtension.handler( {params: {path: EXTENSION_PATH}}, response, @@ -41,6 +45,30 @@ describe('extension', () => { element, `Extension with ID "${extensionId}" should be visible on chrome://extensions`, ); + + // Uninstall the extension + await uninstallExtension.handler( + {params: {id: extensionId!}}, + response, + context, + ); + + const uninstallResponseLine = response.responseLines[1]; + assert.ok( + uninstallResponseLine.includes('Extension uninstalled'), + 'Response should indicate uninstallation', + ); + + await page.waitForSelector('extensions-manager'); + + const elementAfterUninstall = await page.$( + `extensions-manager >>> extensions-item[id="${extensionId}"]`, + ); + assert.strictEqual( + elementAfterUninstall, + null, + `Extension with ID "${extensionId}" should NOT be visible on chrome://extensions`, + ); }); }); });