Skip to content

Commit a17cd71

Browse files
author
Natallia Harshunova
committed
Implement uninstall_extension tool
1 parent ee35f20 commit a17cd71

File tree

6 files changed

+57
-18
lines changed

6 files changed

+57
-18
lines changed

scripts/generate-docs.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -278,11 +278,6 @@ async function generateToolDocumentation(): Promise<void> {
278278
// Convert ToolDefinitions to ToolWithAnnotations
279279
const toolsWithAnnotations: ToolWithAnnotations[] = tools
280280
.filter(tool => {
281-
// Filter out extension tools
282-
if (tool.name === 'install_extension') {
283-
return false;
284-
}
285-
286281
if (!tool.annotations.conditions) {
287282
return true;
288283
}

src/McpContext.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,4 +694,8 @@ export class McpContext implements Context {
694694
async installExtension(path: string): Promise<string> {
695695
return this.browser.installExtension(path);
696696
}
697+
698+
async uninstallExtension(id: string): Promise<void> {
699+
return this.browser.uninstallExtension(id);
700+
}
697701
}

src/tools/ToolDefinition.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export type Context = Readonly<{
125125
*/
126126
resolveCdpElementId(cdpBackendNodeId: number): string | undefined;
127127
installExtension(path: string): Promise<string>;
128+
uninstallExtension(id: string): Promise<void>;
128129
}>;
129130

130131
export function defineTool<Schema extends zod.ZodRawShape>(

src/tools/extensions.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@ import {zod} from '../third_party/index.js';
99
import {ToolCategory} from './categories.js';
1010
import {defineTool} from './ToolDefinition.js';
1111

12+
const EXTENSIONS_CONDITION = 'experimentalExtensionSupport';
13+
1214
export const installExtension = defineTool({
1315
name: 'install_extension',
1416
description: 'Installs a Chrome extension from the given path.',
1517
annotations: {
1618
category: ToolCategory.EXTENSIONS,
1719
readOnlyHint: false,
20+
conditions: [EXTENSIONS_CONDITION],
1821
},
1922
schema: {
2023
path: zod
@@ -27,3 +30,21 @@ export const installExtension = defineTool({
2730
response.appendResponseLine(`Extension installed. Id: ${id}`);
2831
},
2932
});
33+
34+
export const uninstallExtension = defineTool({
35+
name: 'uninstall_extension',
36+
description: 'Uninstalls a Chrome extension by its ID.',
37+
annotations: {
38+
category: ToolCategory.EXTENSIONS,
39+
readOnlyHint: false,
40+
conditions: [EXTENSIONS_CONDITION],
41+
},
42+
schema: {
43+
id: zod.string().describe('ID of the extension to uninstall.'),
44+
},
45+
handler: async (request, response, context) => {
46+
const {id} = request.params;
47+
await context.uninstallExtension(id);
48+
response.appendResponseLine(`Extension uninstalled. Id: ${id}`);
49+
},
50+
});

tests/index.test.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -98,17 +98,7 @@ describe('e2e', () => {
9898
const fileTools = await import(`../src/tools/${file}`);
9999
for (const maybeTool of Object.values<ToolDefinition>(fileTools)) {
100100
if ('name' in maybeTool) {
101-
if (maybeTool.annotations?.conditions?.includes('computerVision')) {
102-
continue;
103-
}
104-
if (
105-
maybeTool.annotations?.conditions?.includes(
106-
'experimentalInteropTools',
107-
)
108-
) {
109-
continue;
110-
}
111-
if (maybeTool.name === 'install_extension') {
101+
if (maybeTool.annotations?.conditions) {
112102
continue;
113103
}
114104
definedNames.push(maybeTool.name);

tests/tools/extensions.test.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import assert from 'node:assert';
88
import path from 'node:path';
99
import {describe, it} from 'node:test';
1010

11-
import {installExtension} from '../../src/tools/extensions.js';
11+
import {
12+
installExtension,
13+
uninstallExtension,
14+
} from '../../src/tools/extensions.js';
1215
import {withMcpContext} from '../utils.js';
1316

1417
const EXTENSION_PATH = path.join(
@@ -17,8 +20,9 @@ const EXTENSION_PATH = path.join(
1720
);
1821

1922
describe('extension', () => {
20-
it('installs an extension and verifies it is listed in chrome://extensions', async () => {
23+
it('installs and uninstalls an extension and verifies it in chrome://extensions', async () => {
2124
await withMcpContext(async (response, context) => {
25+
// Install the extension
2226
await installExtension.handler(
2327
{params: {path: EXTENSION_PATH}},
2428
response,
@@ -41,6 +45,30 @@ describe('extension', () => {
4145
element,
4246
`Extension with ID "${extensionId}" should be visible on chrome://extensions`,
4347
);
48+
49+
// Uninstall the extension
50+
await uninstallExtension.handler(
51+
{params: {id: extensionId!}},
52+
response,
53+
context,
54+
);
55+
56+
const uninstallResponseLine = response.responseLines[1];
57+
assert.ok(
58+
uninstallResponseLine.includes('Extension uninstalled'),
59+
'Response should indicate uninstallation',
60+
);
61+
62+
await page.waitForSelector('extensions-manager');
63+
64+
const elementAfterUninstall = await page.$(
65+
`extensions-manager >>> extensions-item[id="${extensionId}"]`,
66+
);
67+
assert.strictEqual(
68+
elementAfterUninstall,
69+
null,
70+
`Extension with ID "${extensionId}" should NOT be visible on chrome://extensions`,
71+
);
4472
});
4573
});
4674
});

0 commit comments

Comments
 (0)