Skip to content
Merged
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
6 changes: 3 additions & 3 deletions src/McpPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type {
WebMCPTool,
} from './third_party/index.js';
import {takeSnapshot} from './tools/snapshot.js';
import type {ToolGroup, ToolDefinition} from './tools/thirdPartyDeveloper.js';
import type {ToolGroups} from './tools/thirdPartyDeveloper.js';
import type {
ContextPage,
DevToolsData,
Expand Down Expand Up @@ -59,7 +59,7 @@ export class McpPage implements ContextPage {
#dialog?: Dialog;
#dialogHandler: (dialog: Dialog) => void;

thirdPartyDeveloperTools: ToolGroup<ToolDefinition> | undefined;
thirdPartyDeveloperTools: ToolGroups = [];

constructor(page: Page, id: number) {
this.pptrPage = page;
Expand Down Expand Up @@ -90,7 +90,7 @@ export class McpPage implements ContextPage {
}
}

getThirdPartyDeveloperTools(): ToolGroup<ToolDefinition> | undefined {
getThirdPartyDeveloperTools(): ToolGroups {
return this.thirdPartyDeveloperTools;
}

Expand Down
136 changes: 81 additions & 55 deletions src/McpResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import type {
Extension,
} from './third_party/index.js';
import {handleDialog} from './tools/pages.js';
import type {ToolGroup, ToolDefinition} from './tools/thirdPartyDeveloper.js';
import type {ToolGroups} from './tools/thirdPartyDeveloper.js';
import type {
DevToolsData,
ImageContentData,
Expand Down Expand Up @@ -99,9 +99,7 @@ export function replaceHtmlElementsWithUids(schema: JSONSchema7Definition) {
}
}

async function getToolGroup(
page: McpPage,
): Promise<ToolGroup<ToolDefinition> | undefined | null> {
async function getToolGroups(page: McpPage): Promise<ToolGroups> {
// Check if there is a `devtoolstooldiscovery` event listener
const windowHandle = await page.pptrPage.evaluateHandle(() => window);
// @ts-expect-error internal API
Expand All @@ -111,51 +109,88 @@ async function getToolGroup(
objectId: windowHandle.remoteObject().objectId,
});
if (listeners.find(l => l.type === 'devtoolstooldiscovery') === undefined) {
return;
return [];
}

const toolGroup = await page.pptrPage.evaluate(() => {
return new Promise<ToolGroup<ToolDefinition> | undefined | null>(
resolve => {
const event = new CustomEvent('devtoolstooldiscovery');
// @ts-expect-error Adding custom property
event.respondWith = (toolGroup: ToolGroup) => {
if (!window.__dtmcp) {
window.__dtmcp = {};
const toolGroups = await page.pptrPage.evaluate(() => {
return new Promise<ToolGroups>(resolve => {
const event = new CustomEvent('devtoolstooldiscovery');
const groups: ToolGroups = [];
// @ts-expect-error Adding custom property
event.respondWith = toolGroup => {
if (!window.__dtmcp) {
window.__dtmcp = {};
}
if (!window.__dtmcp.toolGroups) {
window.__dtmcp.toolGroups = [];
}

if (
typeof toolGroup.name !== 'string' ||
typeof toolGroup.description !== 'string' ||
!Array.isArray(toolGroup.tools)
) {
console.error('Invalid toolGroup:', toolGroup);
return;
}
for (const tool of toolGroup.tools) {
if (
typeof tool.name !== 'string' ||
typeof tool.description !== 'string' ||
typeof tool.inputSchema !== 'object' ||
typeof tool.execute !== 'function'
) {
console.error('Invalid tool:', tool);
return;
}
window.__dtmcp.toolGroup = toolGroup;
}

// When receiving a toolGroup for the first time, expose a simple execution helper
if (!window.__dtmcp.executeTool) {
window.__dtmcp.executeTool = async (toolName, args) => {
if (!window.__dtmcp?.toolGroup) {
throw new Error('No tools found on the page');
}
const tool = window.__dtmcp.toolGroup.tools.find(
t => t.name === toolName,
);
if (!tool) {
throw new Error(`Tool ${toolName} not found`);
window.__dtmcp.toolGroups.push(toolGroup);

// When receiving a toolGroup for the first time, expose a simple execution helper
if (!window.__dtmcp.executeTool) {
window.__dtmcp.executeTool = async (toolName, args) => {
if (
!window.__dtmcp?.toolGroups ||
window.__dtmcp.toolGroups.length === 0
) {
throw new Error('No tools found on the page');
}
for (const group of window.__dtmcp.toolGroups) {
const tool = group.tools?.find(t => t.name === toolName);
if (tool) {
return await tool.execute(args);
}
return await tool.execute(args);
};
}
}
throw new Error(`Tool ${toolName} not found`);
};
}

resolve(toolGroup);
};
window.dispatchEvent(event);
// If the page does not synchronously call `event.respondWith`, return instead of timing out
groups.push(toolGroup);
};
window.dispatchEvent(event);
// If at least one toolGroup was added synchronously, resolve with the array.
// Otherwise, use setTimeout to allow for any microtask/asynchronous respondWith calls, or resolve with an empty array.
if (groups.length > 0) {
resolve(groups);
} else {
setTimeout(() => {
resolve(null);
if (groups.length > 0) {
resolve(groups);
} else {
resolve([]);
}
}, 0);
},
);
}
});
});

for (const tool of toolGroup?.tools ?? []) {
replaceHtmlElementsWithUids(tool.inputSchema);
for (const group of toolGroups) {
for (const tool of group.tools ?? []) {
replaceHtmlElementsWithUids(tool.inputSchema);
}
}
return toolGroup;
return toolGroups;
}

export class McpResponse implements Response {
Expand Down Expand Up @@ -562,14 +597,13 @@ export class McpResponse implements Response {
extensions = await context.listExtensions();
}

// Null indicates no tools.
let thirdPartyDeveloperTools: ToolGroup<ToolDefinition> | undefined | null;
let thirdPartyDeveloperTools: ToolGroups = [];
if (
this.#args.categoryExperimentalThirdParty &&
this.#listThirdPartyDeveloperTools &&
this.#page
) {
thirdPartyDeveloperTools = await getToolGroup(this.#page);
thirdPartyDeveloperTools = await getToolGroups(this.#page);
if (thirdPartyDeveloperTools) {
this.#page.thirdPartyDeveloperTools = thirdPartyDeveloperTools;
}
Expand Down Expand Up @@ -707,7 +741,7 @@ export class McpResponse implements Response {
traceInsight?: TraceInsightData;
extensions?: Map<string, Extension>;
lighthouseResult?: LighthouseData;
thirdPartyDeveloperTools?: ToolGroup<ToolDefinition> | null;
thirdPartyDeveloperTools: ToolGroups;
webmcpTools?: WebMCPTool[];
errorMessage?: string;
},
Expand All @@ -724,7 +758,7 @@ export class McpResponse implements Response {
traceInsights?: Array<{insightName: string; insightKey: string}>;
lighthouseResult?: object;
extensions?: object[];
thirdPartyDeveloperTools?: object;
thirdPartyDeveloperTools?: object[];
webmcpTools?: object[];
message?: string;
networkConditions?: string;
Expand Down Expand Up @@ -1052,19 +1086,11 @@ Call ${handleDialog.name} to handle it before continuing.`);
}
}

if (data.thirdPartyDeveloperTools !== undefined) {
if (data.thirdPartyDeveloperTools) {
structuredContent.thirdPartyDeveloperTools =
data.thirdPartyDeveloperTools;
}
if (data.thirdPartyDeveloperTools.length) {
structuredContent.thirdPartyDeveloperTools =
data.thirdPartyDeveloperTools;
response.push('## Third-party developer tools');
if (
data.thirdPartyDeveloperTools === null ||
!data.thirdPartyDeveloperTools?.tools
) {
response.push('No third-party developer tools available.');
} else {
const toolGroup = data.thirdPartyDeveloperTools;
for (const toolGroup of data.thirdPartyDeveloperTools) {
response.push(`${toolGroup.name}: ${toolGroup.description}`);
response.push('Available tools:');
const toolDefinitionsMessage = toolGroup.tools
Expand Down
10 changes: 3 additions & 7 deletions src/tools/ToolDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,7 @@ import type {PaginationOptions} from '../utils/types.js';
import type {WaitForEventsResult} from '../WaitForHelper.js';

import type {ToolCategory} from './categories.js';
import type {
ToolGroup,
ToolDefinition as ThirdPartyDeveloperToolDefinition,
} from './thirdPartyDeveloper.js';
import type {ToolGroups} from './thirdPartyDeveloper.js';

export interface BaseToolDefinition<
Schema extends zod.ZodRawShape = zod.ZodRawShape,
Expand Down Expand Up @@ -270,9 +267,8 @@ export type ContextPage = Readonly<{
action: () => Promise<unknown>,
options?: {timeout?: number; handleDialog?: 'accept' | 'dismiss' | string},
): Promise<WaitForEventsResult>;
getThirdPartyDeveloperTools():
| ToolGroup<ThirdPartyDeveloperToolDefinition>
| undefined;
getThirdPartyDeveloperTools(): ToolGroups;

executeThirdPartyDeveloperTool(
toolName: string,
params: Record<string, unknown>,
Expand Down
22 changes: 18 additions & 4 deletions src/tools/thirdPartyDeveloper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,17 @@ export interface ToolGroup<T extends ToolDefinition> {
tools: T[];
}

export type ToolGroups = Array<ToolGroup<ToolDefinition>>;

declare global {
interface Window {
__dtmcp?: {
toolGroup?: ToolGroup<
ToolDefinition & {execute: (args: Record<string, unknown>) => unknown}
toolGroups?: Array<
ToolGroup<
ToolDefinition & {
execute: (args: Record<string, unknown>) => unknown;
}
>
>;
stashedElements?: Element[];
executeTool?: (
Expand Down Expand Up @@ -90,8 +96,16 @@ export const executeThirdPartyDeveloperTool = definePageTool({
}
}

const toolGroup = request.page.getThirdPartyDeveloperTools();
const tool = toolGroup?.tools.find(t => t.name === toolName);
const toolGroups = request.page.getThirdPartyDeveloperTools();
let tool;
if (toolGroups) {
for (const group of toolGroups) {
tool = group.tools?.find(t => t.name === toolName);
if (tool) {
break;
}
}
}
if (!tool) {
throw new Error(`Tool ${toolName} not found`);
}
Expand Down
32 changes: 17 additions & 15 deletions tests/McpResponse.test.js.snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -1261,24 +1261,26 @@ name="myTool", description="Does something", inputSchema={"type":"object","prope

exports[`third-party developer tools > lists third-party developer tools 2`] = `
{
"thirdPartyDeveloperTools": {
"name": "My Tool Group",
"description": "A group of tools",
"tools": [
{
"name": "myTool",
"description": "Does something",
"inputSchema": {
"type": "object",
"properties": {
"foo": {
"type": "string"
"thirdPartyDeveloperTools": [
{
"name": "My Tool Group",
"description": "A group of tools",
"tools": [
{
"name": "myTool",
"description": "Does something",
"inputSchema": {
"type": "object",
"properties": {
"foo": {
"type": "string"
}
}
}
}
}
]
}
]
}
]
}
`;

Expand Down
30 changes: 16 additions & 14 deletions tests/McpResponse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1070,22 +1070,24 @@ describe('third-party developer tools', () => {
async (response, context) => {
const mcpPage = context.getSelectedMcpPage();
stubToolDiscovery(mcpPage.pptrPage);
sinon.stub(mcpPage.pptrPage, 'evaluate').resolves({
name: 'My Tool Group',
description: 'A group of tools',
tools: [
{
name: 'myTool',
description: 'Does something',
inputSchema: {
type: 'object',
properties: {
foo: {type: 'string'},
sinon.stub(mcpPage.pptrPage, 'evaluate').resolves([
{
name: 'My Tool Group',
description: 'A group of tools',
tools: [
{
name: 'myTool',
description: 'Does something',
inputSchema: {
type: 'object',
properties: {
foo: {type: 'string'},
},
},
},
},
],
});
],
},
]);
response.setListThirdPartyDeveloperTools();
const {content, structuredContent} = await response.handle(
'test',
Expand Down
Loading
Loading