豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content
Merged
6 changes: 6 additions & 0 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ jobs:
shell: bash
run: npm ci

- name: Install Chrome Canary
shell: bash
run: |
CANARY_PATH=$(npx @puppeteer/browsers install chrome@canary --format "{{path}}")
echo "CANARY_EXECUTABLE_PATH=$CANARY_PATH" >> $GITHUB_ENV

- name: Build
run: npm run bundle
env:
Expand Down
18 changes: 18 additions & 0 deletions src/McpContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,24 @@ export class McpContext implements Context {
this.#extensionRegistry.remove(id);
}

async triggerExtensionAction(id: string): Promise<void> {
const page = this.getSelectedPptrPage();
// @ts-expect-error internal puppeteer api is needed since we don't have a way to get
// a tab id at the moment
const theTarget = page._tabId;
const session = await this.browser.target().createCDPSession();

try {
// @ts-expect-error triggerAction is not yet available
await session.send('Extensions.triggerAction', {
id,
targetId: theTarget,
});
} finally {
await session.detach();
}
}

listExtensions(): InstalledExtension[] {
return this.#extensionRegistry.list();
}
Expand Down
1 change: 1 addition & 0 deletions src/tools/ToolDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ export type Context = Readonly<{
): void;
installExtension(path: string): Promise<string>;
uninstallExtension(id: string): Promise<void>;
triggerExtensionAction(id: string): Promise<void>;
listExtensions(): InstalledExtension[];
getExtension(id: string): InstalledExtension | undefined;
getSelectedMcpPage(): McpPage;
Expand Down
18 changes: 18 additions & 0 deletions src/tools/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,21 @@ export const reloadExtension = defineTool({
response.appendResponseLine('Extension reloaded.');
},
});

export const triggerExtensionAction = defineTool({
name: 'trigger_extension_action',
description: 'Triggers an action in a Chrome extension.',
annotations: {
category: ToolCategory.EXTENSIONS,
readOnlyHint: false,
conditions: [EXTENSIONS_CONDITION],
},
schema: {
id: zod.string().describe('ID of the extension.'),
},
handler: async (request, response, context) => {
const {id} = request.params;
await context.triggerExtensionAction(id);
response.appendResponseLine(`Extension action triggered. Id: ${id}`);
},
});
75 changes: 75 additions & 0 deletions tests/tools/extensions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,22 @@ import {afterEach, describe, it} from 'node:test';

import sinon from 'sinon';

import type {ParsedArguments} from '../../src/cli.js';
import type {McpResponse} from '../../src/McpResponse.js';
import {
installExtension,
uninstallExtension,
listExtensions,
reloadExtension,
triggerExtensionAction,
} from '../../src/tools/extensions.js';
import {listPages} from '../../src/tools/pages.js';
import {withMcpContext} from '../utils.js';

const EXTENSION_WITH_SW_PATH = path.join(
import.meta.dirname,
'../../../tests/tools/fixtures/extension-sw',
);
const EXTENSION_PATH = path.join(
import.meta.dirname,
'../../../tests/tools/fixtures/extension',
Expand Down Expand Up @@ -128,4 +135,72 @@ describe('extension', () => {
assert.ok(reinstalled, 'Extension should be present after reload');
});
});
it('triggers an extension action', async () => {
await withMcpContext(
async (response, context) => {
await installExtension.handler(
Comment thread
nroscino marked this conversation as resolved.
Outdated
{params: {path: EXTENSION_WITH_SW_PATH}},
response,
context,
);

const extensionId = extractId(response);

response.resetResponseLineForTesting();
Comment thread
nroscino marked this conversation as resolved.
Outdated
const listPageDef = listPages({
categoryExtensions: true,
} as ParsedArguments);
await listPageDef.handler(
{params: {}, page: context.getSelectedMcpPage()},
response,
context,
);
let result = await response.handle(listPageDef.name, context);
let textContent = result.content.find(c => c.type === 'text') as {
type: 'text';
text: string;
};
assert.ok(
!textContent.text.includes(extensionId),
'Response should not contain extension service worker id',
);

await triggerExtensionAction.handler(
{params: {id: extensionId}},
response,
context,
);

const swTarget = await context.browser.waitForTarget(
t => t.type() === 'service_worker' && t.url().includes(extensionId),
);
const swUrl = swTarget.url();

response.resetResponseLineForTesting();
await listPageDef.handler(
{params: {}, page: context.getSelectedMcpPage()},
response,
context,
);
result = await response.handle(listPageDef.name, context);
textContent = result.content.find(c => c.type === 'text') as {
Comment thread
nroscino marked this conversation as resolved.
Outdated
type: 'text';
text: string;
};
assert.ok(
textContent.text.includes(swUrl),
'Response should contain extension service worker url',
);
},
{
channel: process.env.CANARY_EXECUTABLE_PATH
Comment thread
nroscino marked this conversation as resolved.
Outdated
? undefined
: 'chrome-canary',
executablePath: process.env.CANARY_EXECUTABLE_PATH,
},
{
categoryExtensions: true,
} as ParsedArguments,
);
});
});
4 changes: 3 additions & 1 deletion tests/tools/fixtures/extension-sw/sw.js
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
console.log('Service worker loaded');
chrome.action.onClicked.addListener(tab => {
console.log('Action clicked');
});
16 changes: 14 additions & 2 deletions tests/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import logger from 'debug';
import type {Browser} from 'puppeteer';
import puppeteer, {Locator} from 'puppeteer';
import type {
ChromeReleaseChannel,
Frame,
HTTPRequest,
HTTPResponse,
Expand Down Expand Up @@ -47,10 +48,19 @@ let context: McpContext | undefined;

export async function withBrowser(
cb: (browser: Browser, page: Page) => Promise<void>,
options: {debug?: boolean; autoOpenDevTools?: boolean} = {},
options: {
debug?: boolean;
autoOpenDevTools?: boolean;
channel?: string;
Comment thread
nroscino marked this conversation as resolved.
Outdated
executablePath?: string;
} = {},
) {
const launchOptions: LaunchOptions = {
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH,
executablePath:
options.executablePath ?? process.env.PUPPETEER_EXECUTABLE_PATH,
channel: (options.executablePath
? undefined
: options.channel) as ChromeReleaseChannel,
headless: !options.debug,
defaultViewport: null,
devtools: options.autoOpenDevTools ?? false,
Expand Down Expand Up @@ -85,6 +95,8 @@ export async function withMcpContext(
debug?: boolean;
autoOpenDevTools?: boolean;
performanceCrux?: boolean;
channel?: string;
Comment thread
nroscino marked this conversation as resolved.
Outdated
executablePath?: string;
} = {},
args: ParsedArguments = {} as ParsedArguments,
) {
Expand Down