Skip to content

Commit c86de45

Browse files
committed
feat: implement trigger extension action
1 parent 505089c commit c86de45

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
@@ -939,6 +939,24 @@ export class McpContext implements Context {
939939
this.#extensionRegistry.remove(id);
940940
}
941941

942+
async triggerExtensionAction(id: string): Promise<void> {
943+
const page = this.getSelectedPage();
944+
// @ts-expect-error internal puppeteer api is needed since we don't have a way to get
945+
// a tab id at the moment
946+
const theTarget = page._tabId;
947+
const session = await this.browser.target().createCDPSession();
948+
949+
try {
950+
// @ts-expect-error triggerAction is not yet available
951+
await session.send('Extensions.triggerAction', {
952+
id,
953+
targetId: theTarget,
954+
});
955+
} finally {
956+
await session.detach();
957+
}
958+
}
959+
942960
listExtensions(): InstalledExtension[] {
943961
return this.#extensionRegistry.list();
944962
}

src/tools/ToolDefinition.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ export type Context = Readonly<{
190190
): void;
191191
installExtension(path: string): Promise<string>;
192192
uninstallExtension(id: string): Promise<void>;
193+
triggerExtensionAction(id: string): Promise<void>;
193194
listExtensions(): InstalledExtension[];
194195
getExtension(id: string): InstalledExtension | undefined;
195196
getSelectedMcpPage(): McpPage;

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)