Skip to content

Commit d0ec2ea

Browse files
committed
feat: implement trigger extension action
1 parent a8bf3e5 commit d0ec2ea

5 files changed

Lines changed: 76 additions & 1 deletion

File tree

src/McpContext.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -988,6 +988,24 @@ export class McpContext implements Context {
988988
this.#extensionRegistry.remove(id);
989989
}
990990

991+
async triggerExtensionAction(id: string): Promise<void> {
992+
const page = this.getSelectedPage();
993+
// @ts-expect-error internal puppeteer api is needed since we don't have a way to get
994+
// a tab id at the moment
995+
const theTarget = page._tabId;
996+
const session = await this.browser.target().createCDPSession();
997+
998+
try {
999+
// @ts-expect-error triggerAction is not yet available
1000+
await session.send('Extensions.triggerAction', {
1001+
id,
1002+
targetId: theTarget,
1003+
});
1004+
} finally {
1005+
await session.detach();
1006+
}
1007+
}
1008+
9911009
listExtensions(): InstalledExtension[] {
9921010
return this.#extensionRegistry.list();
9931011
}

src/tools/ToolDefinition.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ export type Context = Readonly<{
152152
): void;
153153
installExtension(path: string): Promise<string>;
154154
uninstallExtension(id: string): Promise<void>;
155+
triggerExtensionAction(id: string): Promise<void>;
155156
listExtensions(): InstalledExtension[];
156157
getExtension(id: string): InstalledExtension | undefined;
157158
}>;

src/tools/extensions.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,21 @@ export const reloadExtension = defineTool({
8585
response.appendResponseLine('Extension reloaded.');
8686
},
8787
});
88+
89+
export const triggerExtensionAction = defineTool({
90+
name: 'trigger_extension_action',
91+
description: 'Triggers an action in a Chrome extension.',
92+
annotations: {
93+
category: ToolCategory.EXTENSIONS,
94+
readOnlyHint: false,
95+
conditions: [EXTENSIONS_CONDITION],
96+
},
97+
schema: {
98+
id: zod.string().describe('ID of the extension.'),
99+
},
100+
handler: async (request, response, context) => {
101+
const {id} = request.params;
102+
await context.triggerExtensionAction(id);
103+
response.appendResponseLine(`Extension action triggered. Id: ${id}`);
104+
},
105+
});

tests/tools/extensions.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
uninstallExtension,
1717
listExtensions,
1818
reloadExtension,
19+
triggerExtensionAction,
1920
} from '../../src/tools/extensions.js';
2021
import {withMcpContext} from '../utils.js';
2122

@@ -128,4 +129,39 @@ describe('extension', () => {
128129
assert.ok(reinstalled, 'Extension should be present after reload');
129130
});
130131
});
132+
133+
it('triggers an extension action', async () => {
134+
await withMcpContext(
135+
async (response, context) => {
136+
const triggerSpy = sinon.spy(context, 'triggerExtensionAction');
137+
138+
await installExtension.handler(
139+
{params: {path: EXTENSION_PATH}},
140+
response,
141+
context,
142+
);
143+
144+
const extensionId = extractId(response);
145+
response.resetResponseLineForTesting();
146+
147+
await triggerExtensionAction.handler(
148+
{params: {id: extensionId}},
149+
response,
150+
context,
151+
);
152+
153+
assert.ok(
154+
triggerSpy.calledOnceWithExactly(extensionId),
155+
'triggerExtensionAction should be called with correct params',
156+
);
157+
assert.ok(
158+
response.responseLines[0].includes(
159+
`Extension action triggered. Id: ${extensionId}`,
160+
),
161+
'Response should indicate action triggered',
162+
);
163+
},
164+
{channel: 'chrome-canary'},
165+
);
166+
});
131167
});

tests/utils.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,11 @@ let context: McpContext | undefined;
4747

4848
export async function withBrowser(
4949
cb: (browser: Browser, page: Page) => Promise<void>,
50-
options: {debug?: boolean; autoOpenDevTools?: boolean} = {},
50+
options: {debug?: boolean; autoOpenDevTools?: boolean; channel?: string} = {},
5151
) {
5252
const launchOptions: LaunchOptions = {
5353
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH,
54+
channel: options.channel as any,
5455
headless: !options.debug,
5556
defaultViewport: null,
5657
devtools: options.autoOpenDevTools ?? false,
@@ -85,6 +86,7 @@ export async function withMcpContext(
8586
debug?: boolean;
8687
autoOpenDevTools?: boolean;
8788
performanceCrux?: boolean;
89+
channel?: string;
8890
} = {},
8991
args: ParsedArguments = {} as ParsedArguments,
9092
) {

0 commit comments

Comments
 (0)