Skip to content
Open
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
5 changes: 0 additions & 5 deletions scripts/generate-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,11 +278,6 @@ async function generateToolDocumentation(): Promise<void> {
// 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;
}
Expand Down
4 changes: 4 additions & 0 deletions src/McpContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -694,4 +694,8 @@ export class McpContext implements Context {
async installExtension(path: string): Promise<string> {
return this.browser.installExtension(path);
}

async uninstallExtension(id: string): Promise<void> {
return this.browser.uninstallExtension(id);
}
}
1 change: 1 addition & 0 deletions src/tools/ToolDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export type Context = Readonly<{
*/
resolveCdpElementId(cdpBackendNodeId: number): string | undefined;
installExtension(path: string): Promise<string>;
uninstallExtension(id: string): Promise<void>;
}>;

export function defineTool<Schema extends zod.ZodRawShape>(
Expand Down
21 changes: 21 additions & 0 deletions src/tools/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}`);
},
});
12 changes: 1 addition & 11 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,17 +98,7 @@ describe('e2e', () => {
const fileTools = await import(`../src/tools/${file}`);
for (const maybeTool of Object.values<ToolDefinition>(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);
Expand Down
32 changes: 30 additions & 2 deletions tests/tools/extensions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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,
Expand All @@ -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`,
);
});
});
});