refactor: move tool groups (#2365)
Moves the getter for tool groups to McpPage.
This commit is contained in:
+150
@@ -4,6 +4,57 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
export function replaceHtmlElementsWithUids(schema: JSONSchema7Definition) {
|
||||
if (typeof schema === 'boolean') {
|
||||
return;
|
||||
}
|
||||
|
||||
let isHtmlElement = false;
|
||||
for (const [key, value] of Object.entries(schema)) {
|
||||
if (key === 'x-mcp-type' && value === 'HTMLElement') {
|
||||
isHtmlElement = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isHtmlElement) {
|
||||
schema.properties = {uid: {type: 'string'}};
|
||||
schema.required = ['uid'];
|
||||
}
|
||||
|
||||
if (schema.properties) {
|
||||
for (const key of Object.keys(schema.properties)) {
|
||||
replaceHtmlElementsWithUids(schema.properties[key]);
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.items) {
|
||||
if (Array.isArray(schema.items)) {
|
||||
for (const item of schema.items) {
|
||||
replaceHtmlElementsWithUids(item);
|
||||
}
|
||||
} else {
|
||||
replaceHtmlElementsWithUids(schema.items);
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.anyOf) {
|
||||
for (const s of schema.anyOf) {
|
||||
replaceHtmlElementsWithUids(s);
|
||||
}
|
||||
}
|
||||
if (schema.allOf) {
|
||||
for (const s of schema.allOf) {
|
||||
replaceHtmlElementsWithUids(s);
|
||||
}
|
||||
}
|
||||
if (schema.oneOf) {
|
||||
for (const s of schema.oneOf) {
|
||||
replaceHtmlElementsWithUids(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
import {
|
||||
createTargetUniverse,
|
||||
type TargetUniverse,
|
||||
@@ -27,6 +78,7 @@ import {
|
||||
type ConsoleMessage,
|
||||
type HTTPRequest,
|
||||
type DevTools,
|
||||
type JSONSchema7Definition,
|
||||
} from './third_party/index.js';
|
||||
import {takeSnapshot} from './tools/snapshot.js';
|
||||
import type {ToolGroups} from './tools/thirdPartyDeveloper.js';
|
||||
@@ -171,6 +223,104 @@ export class McpPage implements ContextPage {
|
||||
return this.thirdPartyDeveloperTools;
|
||||
}
|
||||
|
||||
async getToolGroups(): Promise<ToolGroups> {
|
||||
// Check if there is a `devtoolstooldiscovery` event listener
|
||||
const windowHandle = await this.pptrPage.evaluateHandle(() => window);
|
||||
// @ts-expect-error internal API
|
||||
const client = this.pptrPage._client();
|
||||
const {listeners}: {listeners: Protocol.DOMDebugger.EventListener[]} =
|
||||
await client.send('DOMDebugger.getEventListeners', {
|
||||
objectId: windowHandle.remoteObject().objectId,
|
||||
});
|
||||
if (listeners.find(l => l.type === 'devtoolstooldiscovery') === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const toolGroups = await this.pptrPage.evaluate(() => {
|
||||
if (window.__dtmcp) {
|
||||
window.__dtmcp.toolGroups = [];
|
||||
}
|
||||
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' ||
|
||||
(toolGroup.description &&
|
||||
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.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);
|
||||
}
|
||||
}
|
||||
throw new Error(`Tool ${toolName} not found`);
|
||||
};
|
||||
}
|
||||
|
||||
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(() => {
|
||||
if (groups.length > 0) {
|
||||
resolve(groups);
|
||||
} else {
|
||||
resolve([]);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
for (const group of toolGroups) {
|
||||
for (const tool of group.tools ?? []) {
|
||||
replaceHtmlElementsWithUids(tool.inputSchema);
|
||||
}
|
||||
}
|
||||
return toolGroups;
|
||||
}
|
||||
|
||||
getWebMcpTools(): WebMCPTool[] {
|
||||
return this.pptrPage.webmcp.tools();
|
||||
}
|
||||
|
||||
+2
-157
@@ -26,19 +26,13 @@ import type {McpContext} from './McpContext.js';
|
||||
import type {McpPage} from './McpPage.js';
|
||||
import {UncaughtError} from './PageCollector.js';
|
||||
import {TextSnapshot} from './TextSnapshot.js';
|
||||
import {
|
||||
DevTools,
|
||||
getToonEncode,
|
||||
getGcfEncode,
|
||||
type Protocol,
|
||||
} from './third_party/index.js';
|
||||
import {DevTools, getToonEncode, getGcfEncode} from './third_party/index.js';
|
||||
import type {
|
||||
ConsoleMessage,
|
||||
ImageContent,
|
||||
Page,
|
||||
ResourceType,
|
||||
TextContent,
|
||||
JSONSchema7Definition,
|
||||
Extension,
|
||||
HTTPRequest,
|
||||
} from './third_party/index.js';
|
||||
@@ -67,155 +61,6 @@ interface TraceInsightData {
|
||||
insightName: InsightName;
|
||||
}
|
||||
|
||||
export function replaceHtmlElementsWithUids(schema: JSONSchema7Definition) {
|
||||
if (typeof schema === 'boolean') {
|
||||
return;
|
||||
}
|
||||
|
||||
let isHtmlElement = false;
|
||||
for (const [key, value] of Object.entries(schema)) {
|
||||
if (key === 'x-mcp-type' && value === 'HTMLElement') {
|
||||
isHtmlElement = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isHtmlElement) {
|
||||
schema.properties = {uid: {type: 'string'}};
|
||||
schema.required = ['uid'];
|
||||
}
|
||||
|
||||
if (schema.properties) {
|
||||
for (const key of Object.keys(schema.properties)) {
|
||||
replaceHtmlElementsWithUids(schema.properties[key]);
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.items) {
|
||||
if (Array.isArray(schema.items)) {
|
||||
for (const item of schema.items) {
|
||||
replaceHtmlElementsWithUids(item);
|
||||
}
|
||||
} else {
|
||||
replaceHtmlElementsWithUids(schema.items);
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.anyOf) {
|
||||
for (const s of schema.anyOf) {
|
||||
replaceHtmlElementsWithUids(s);
|
||||
}
|
||||
}
|
||||
if (schema.allOf) {
|
||||
for (const s of schema.allOf) {
|
||||
replaceHtmlElementsWithUids(s);
|
||||
}
|
||||
}
|
||||
if (schema.oneOf) {
|
||||
for (const s of schema.oneOf) {
|
||||
replaceHtmlElementsWithUids(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
const client = page.pptrPage._client();
|
||||
const {listeners}: {listeners: Protocol.DOMDebugger.EventListener[]} =
|
||||
await client.send('DOMDebugger.getEventListeners', {
|
||||
objectId: windowHandle.remoteObject().objectId,
|
||||
});
|
||||
if (listeners.find(l => l.type === 'devtoolstooldiscovery') === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const toolGroups = await page.pptrPage.evaluate(() => {
|
||||
if (window.__dtmcp) {
|
||||
window.__dtmcp.toolGroups = [];
|
||||
}
|
||||
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' ||
|
||||
(toolGroup.description &&
|
||||
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.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);
|
||||
}
|
||||
}
|
||||
throw new Error(`Tool ${toolName} not found`);
|
||||
};
|
||||
}
|
||||
|
||||
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(() => {
|
||||
if (groups.length > 0) {
|
||||
resolve(groups);
|
||||
} else {
|
||||
resolve([]);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
for (const group of toolGroups) {
|
||||
for (const tool of group.tools ?? []) {
|
||||
replaceHtmlElementsWithUids(tool.inputSchema);
|
||||
}
|
||||
}
|
||||
return toolGroups;
|
||||
}
|
||||
|
||||
export class McpResponse implements Response {
|
||||
#includePages = false;
|
||||
#includeExtensionServiceWorkers = false;
|
||||
@@ -685,7 +530,7 @@ export class McpResponse implements Response {
|
||||
this.#listThirdPartyDeveloperTools &&
|
||||
this.#page
|
||||
) {
|
||||
thirdPartyDeveloperTools = await getToolGroups(this.#page);
|
||||
thirdPartyDeveloperTools = await this.#page.getToolGroups();
|
||||
if (thirdPartyDeveloperTools) {
|
||||
this.#page.thirdPartyDeveloperTools = thirdPartyDeveloperTools;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import assert from 'node:assert';
|
||||
import {describe, it} from 'node:test';
|
||||
|
||||
import {replaceHtmlElementsWithUids} from '../src/McpPage.js';
|
||||
import type {JSONSchema7Definition} from '../src/third_party/index.js';
|
||||
|
||||
describe('replaceHtmlElementsWithUids', () => {
|
||||
it('does nothing for boolean schemas', () => {
|
||||
const schemaTrue: JSONSchema7Definition = true;
|
||||
const schemaFalse: JSONSchema7Definition = false;
|
||||
|
||||
replaceHtmlElementsWithUids(schemaTrue);
|
||||
replaceHtmlElementsWithUids(schemaFalse);
|
||||
|
||||
assert.strictEqual(schemaTrue, true);
|
||||
assert.strictEqual(schemaFalse, false);
|
||||
});
|
||||
|
||||
it('replaces HTMLElement type with uid string', () => {
|
||||
const schema: JSONSchema7Definition = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
foo: {type: 'string'},
|
||||
bar: {type: 'number'},
|
||||
},
|
||||
required: ['foo'],
|
||||
};
|
||||
Object.assign(schema, {'x-mcp-type': 'HTMLElement'});
|
||||
|
||||
replaceHtmlElementsWithUids(schema);
|
||||
|
||||
if (typeof schema === 'object') {
|
||||
assert.deepStrictEqual(schema.properties, {
|
||||
uid: {type: 'string'},
|
||||
});
|
||||
assert.deepStrictEqual(schema.required, ['uid']);
|
||||
} else {
|
||||
assert.fail('Schema should be an object');
|
||||
}
|
||||
});
|
||||
|
||||
it('does not replace if x-mcp-type is not HTMLElement', () => {
|
||||
const schema: JSONSchema7Definition = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
foo: {type: 'string'},
|
||||
},
|
||||
};
|
||||
Object.assign(schema, {'x-mcp-type': 'OtherType'});
|
||||
|
||||
replaceHtmlElementsWithUids(schema);
|
||||
|
||||
if (typeof schema === 'object') {
|
||||
assert.deepStrictEqual(schema.properties, {
|
||||
foo: {type: 'string'},
|
||||
});
|
||||
assert.strictEqual(schema.required, undefined);
|
||||
} else {
|
||||
assert.fail('Schema should be an object');
|
||||
}
|
||||
});
|
||||
|
||||
it('recurses into nested properties', () => {
|
||||
const schema: JSONSchema7Definition = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
element: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
foo: {type: 'string'},
|
||||
},
|
||||
},
|
||||
other: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
};
|
||||
if (typeof schema === 'object' && schema.properties) {
|
||||
Object.assign(schema.properties.element, {'x-mcp-type': 'HTMLElement'});
|
||||
}
|
||||
|
||||
replaceHtmlElementsWithUids(schema);
|
||||
|
||||
if (
|
||||
typeof schema === 'object' &&
|
||||
schema.properties &&
|
||||
typeof schema.properties.element === 'object'
|
||||
) {
|
||||
const elementSchema = schema.properties.element;
|
||||
assert.deepStrictEqual(elementSchema.properties, {
|
||||
uid: {type: 'string'},
|
||||
});
|
||||
assert.deepStrictEqual(elementSchema.required, ['uid']);
|
||||
} else {
|
||||
assert.fail('Unexpected schema structure');
|
||||
}
|
||||
});
|
||||
|
||||
it('recurses into array items (single schema object)', () => {
|
||||
const schema: JSONSchema7Definition = {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
},
|
||||
};
|
||||
if (typeof schema === 'object' && typeof schema.items === 'object') {
|
||||
Object.assign(schema.items, {'x-mcp-type': 'HTMLElement'});
|
||||
}
|
||||
|
||||
replaceHtmlElementsWithUids(schema);
|
||||
|
||||
if (typeof schema === 'object' && typeof schema.items === 'object') {
|
||||
const itemsSchema = schema.items;
|
||||
if (!Array.isArray(itemsSchema)) {
|
||||
assert.deepStrictEqual(itemsSchema.properties, {
|
||||
uid: {type: 'string'},
|
||||
});
|
||||
assert.deepStrictEqual(itemsSchema.required, ['uid']);
|
||||
} else {
|
||||
assert.fail('items should not be an array in this test case');
|
||||
}
|
||||
} else {
|
||||
assert.fail('Unexpected schema structure');
|
||||
}
|
||||
});
|
||||
|
||||
it('recurses into array items (array of schemas)', () => {
|
||||
const schema: JSONSchema7Definition = {
|
||||
type: 'array',
|
||||
items: [
|
||||
{
|
||||
type: 'object',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
};
|
||||
if (typeof schema === 'object' && Array.isArray(schema.items)) {
|
||||
Object.assign(schema.items[0], {'x-mcp-type': 'HTMLElement'});
|
||||
}
|
||||
|
||||
replaceHtmlElementsWithUids(schema);
|
||||
|
||||
if (typeof schema === 'object' && Array.isArray(schema.items)) {
|
||||
const firstItem = schema.items[0];
|
||||
if (typeof firstItem === 'object') {
|
||||
assert.deepStrictEqual(firstItem.properties, {
|
||||
uid: {type: 'string'},
|
||||
});
|
||||
assert.deepStrictEqual(firstItem.required, ['uid']);
|
||||
} else {
|
||||
assert.fail('First item should be an object');
|
||||
}
|
||||
|
||||
const secondItem = schema.items[1];
|
||||
if (typeof secondItem === 'object') {
|
||||
assert.strictEqual(secondItem.properties, undefined);
|
||||
} else {
|
||||
assert.fail('Second item should be an object');
|
||||
}
|
||||
} else {
|
||||
assert.fail('Unexpected schema structure');
|
||||
}
|
||||
});
|
||||
|
||||
it('recurses into anyOf', () => {
|
||||
const schema: JSONSchema7Definition = {
|
||||
anyOf: [
|
||||
{
|
||||
type: 'object',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
};
|
||||
if (typeof schema === 'object' && Array.isArray(schema.anyOf)) {
|
||||
Object.assign(schema.anyOf[0], {'x-mcp-type': 'HTMLElement'});
|
||||
}
|
||||
|
||||
replaceHtmlElementsWithUids(schema);
|
||||
|
||||
if (typeof schema === 'object' && Array.isArray(schema.anyOf)) {
|
||||
const firstItem = schema.anyOf[0];
|
||||
if (typeof firstItem === 'object') {
|
||||
assert.deepStrictEqual(firstItem.properties, {
|
||||
uid: {type: 'string'},
|
||||
});
|
||||
} else {
|
||||
assert.fail('First item should be an object');
|
||||
}
|
||||
} else {
|
||||
assert.fail('Unexpected schema structure');
|
||||
}
|
||||
});
|
||||
|
||||
it('recurses into allOf', () => {
|
||||
const schema: JSONSchema7Definition = {
|
||||
allOf: [
|
||||
{
|
||||
type: 'object',
|
||||
},
|
||||
],
|
||||
};
|
||||
if (typeof schema === 'object' && Array.isArray(schema.allOf)) {
|
||||
Object.assign(schema.allOf[0], {'x-mcp-type': 'HTMLElement'});
|
||||
}
|
||||
|
||||
replaceHtmlElementsWithUids(schema);
|
||||
|
||||
if (typeof schema === 'object' && Array.isArray(schema.allOf)) {
|
||||
const firstItem = schema.allOf[0];
|
||||
if (typeof firstItem === 'object') {
|
||||
assert.deepStrictEqual(firstItem.properties, {
|
||||
uid: {type: 'string'},
|
||||
});
|
||||
} else {
|
||||
assert.fail('First item should be an object');
|
||||
}
|
||||
} else {
|
||||
assert.fail('Unexpected schema structure');
|
||||
}
|
||||
});
|
||||
|
||||
it('recurses into oneOf', () => {
|
||||
const schema: JSONSchema7Definition = {
|
||||
oneOf: [
|
||||
{
|
||||
type: 'object',
|
||||
},
|
||||
],
|
||||
};
|
||||
if (typeof schema === 'object' && Array.isArray(schema.oneOf)) {
|
||||
Object.assign(schema.oneOf[0], {'x-mcp-type': 'HTMLElement'});
|
||||
}
|
||||
|
||||
replaceHtmlElementsWithUids(schema);
|
||||
|
||||
if (typeof schema === 'object' && Array.isArray(schema.oneOf)) {
|
||||
const firstItem = schema.oneOf[0];
|
||||
if (typeof firstItem === 'object') {
|
||||
assert.deepStrictEqual(firstItem.properties, {
|
||||
uid: {type: 'string'},
|
||||
});
|
||||
} else {
|
||||
assert.fail('First item should be an object');
|
||||
}
|
||||
} else {
|
||||
assert.fail('Unexpected schema structure');
|
||||
}
|
||||
});
|
||||
});
|
||||
+1
-253
@@ -15,11 +15,7 @@ import sinon from 'sinon';
|
||||
import type {ParsedArguments} from '../src/bin/chrome-devtools-mcp-cli-options.js';
|
||||
import type {McpContext} from '../src/McpContext.js';
|
||||
import type {McpResponse} from '../src/McpResponse.js';
|
||||
import {replaceHtmlElementsWithUids} from '../src/McpResponse.js';
|
||||
import type {
|
||||
Extension,
|
||||
JSONSchema7Definition,
|
||||
} from '../src/third_party/index.js';
|
||||
import type {Extension} from '../src/third_party/index.js';
|
||||
import {
|
||||
closePage,
|
||||
listPages,
|
||||
@@ -1254,254 +1250,6 @@ describe('third-party developer tools', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceHtmlElementsWithUids', () => {
|
||||
it('does nothing for boolean schemas', () => {
|
||||
const schemaTrue: JSONSchema7Definition = true;
|
||||
const schemaFalse: JSONSchema7Definition = false;
|
||||
|
||||
replaceHtmlElementsWithUids(schemaTrue);
|
||||
replaceHtmlElementsWithUids(schemaFalse);
|
||||
|
||||
assert.strictEqual(schemaTrue, true);
|
||||
assert.strictEqual(schemaFalse, false);
|
||||
});
|
||||
|
||||
it('replaces HTMLElement type with uid string', () => {
|
||||
const schema: JSONSchema7Definition = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
foo: {type: 'string'},
|
||||
bar: {type: 'number'},
|
||||
},
|
||||
required: ['foo'],
|
||||
};
|
||||
Object.assign(schema, {'x-mcp-type': 'HTMLElement'});
|
||||
|
||||
replaceHtmlElementsWithUids(schema);
|
||||
|
||||
if (typeof schema === 'object') {
|
||||
assert.deepStrictEqual(schema.properties, {
|
||||
uid: {type: 'string'},
|
||||
});
|
||||
assert.deepStrictEqual(schema.required, ['uid']);
|
||||
} else {
|
||||
assert.fail('Schema should be an object');
|
||||
}
|
||||
});
|
||||
|
||||
it('does not replace if x-mcp-type is not HTMLElement', () => {
|
||||
const schema: JSONSchema7Definition = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
foo: {type: 'string'},
|
||||
},
|
||||
};
|
||||
Object.assign(schema, {'x-mcp-type': 'OtherType'});
|
||||
|
||||
replaceHtmlElementsWithUids(schema);
|
||||
|
||||
if (typeof schema === 'object') {
|
||||
assert.deepStrictEqual(schema.properties, {
|
||||
foo: {type: 'string'},
|
||||
});
|
||||
assert.strictEqual(schema.required, undefined);
|
||||
} else {
|
||||
assert.fail('Schema should be an object');
|
||||
}
|
||||
});
|
||||
|
||||
it('recurses into nested properties', () => {
|
||||
const schema: JSONSchema7Definition = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
element: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
foo: {type: 'string'},
|
||||
},
|
||||
},
|
||||
other: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
};
|
||||
if (typeof schema === 'object' && schema.properties) {
|
||||
Object.assign(schema.properties.element, {'x-mcp-type': 'HTMLElement'});
|
||||
}
|
||||
|
||||
replaceHtmlElementsWithUids(schema);
|
||||
|
||||
if (
|
||||
typeof schema === 'object' &&
|
||||
schema.properties &&
|
||||
typeof schema.properties.element === 'object'
|
||||
) {
|
||||
const elementSchema = schema.properties.element;
|
||||
assert.deepStrictEqual(elementSchema.properties, {
|
||||
uid: {type: 'string'},
|
||||
});
|
||||
assert.deepStrictEqual(elementSchema.required, ['uid']);
|
||||
} else {
|
||||
assert.fail('Unexpected schema structure');
|
||||
}
|
||||
});
|
||||
|
||||
it('recurses into array items (single schema object)', () => {
|
||||
const schema: JSONSchema7Definition = {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
},
|
||||
};
|
||||
if (typeof schema === 'object' && typeof schema.items === 'object') {
|
||||
Object.assign(schema.items, {'x-mcp-type': 'HTMLElement'});
|
||||
}
|
||||
|
||||
replaceHtmlElementsWithUids(schema);
|
||||
|
||||
if (typeof schema === 'object' && typeof schema.items === 'object') {
|
||||
const itemsSchema = schema.items;
|
||||
if (!Array.isArray(itemsSchema)) {
|
||||
assert.deepStrictEqual(itemsSchema.properties, {
|
||||
uid: {type: 'string'},
|
||||
});
|
||||
assert.deepStrictEqual(itemsSchema.required, ['uid']);
|
||||
} else {
|
||||
assert.fail('items should not be an array in this test case');
|
||||
}
|
||||
} else {
|
||||
assert.fail('Unexpected schema structure');
|
||||
}
|
||||
});
|
||||
|
||||
it('recurses into array items (array of schemas)', () => {
|
||||
const schema: JSONSchema7Definition = {
|
||||
type: 'array',
|
||||
items: [
|
||||
{
|
||||
type: 'object',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
};
|
||||
if (typeof schema === 'object' && Array.isArray(schema.items)) {
|
||||
Object.assign(schema.items[0], {'x-mcp-type': 'HTMLElement'});
|
||||
}
|
||||
|
||||
replaceHtmlElementsWithUids(schema);
|
||||
|
||||
if (typeof schema === 'object' && Array.isArray(schema.items)) {
|
||||
const firstItem = schema.items[0];
|
||||
if (typeof firstItem === 'object') {
|
||||
assert.deepStrictEqual(firstItem.properties, {
|
||||
uid: {type: 'string'},
|
||||
});
|
||||
assert.deepStrictEqual(firstItem.required, ['uid']);
|
||||
} else {
|
||||
assert.fail('First item should be an object');
|
||||
}
|
||||
|
||||
const secondItem = schema.items[1];
|
||||
if (typeof secondItem === 'object') {
|
||||
assert.strictEqual(secondItem.properties, undefined);
|
||||
} else {
|
||||
assert.fail('Second item should be an object');
|
||||
}
|
||||
} else {
|
||||
assert.fail('Unexpected schema structure');
|
||||
}
|
||||
});
|
||||
|
||||
it('recurses into anyOf', () => {
|
||||
const schema: JSONSchema7Definition = {
|
||||
anyOf: [
|
||||
{
|
||||
type: 'object',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
};
|
||||
if (typeof schema === 'object' && Array.isArray(schema.anyOf)) {
|
||||
Object.assign(schema.anyOf[0], {'x-mcp-type': 'HTMLElement'});
|
||||
}
|
||||
|
||||
replaceHtmlElementsWithUids(schema);
|
||||
|
||||
if (typeof schema === 'object' && Array.isArray(schema.anyOf)) {
|
||||
const firstItem = schema.anyOf[0];
|
||||
if (typeof firstItem === 'object') {
|
||||
assert.deepStrictEqual(firstItem.properties, {
|
||||
uid: {type: 'string'},
|
||||
});
|
||||
} else {
|
||||
assert.fail('First item should be an object');
|
||||
}
|
||||
} else {
|
||||
assert.fail('Unexpected schema structure');
|
||||
}
|
||||
});
|
||||
|
||||
it('recurses into allOf', () => {
|
||||
const schema: JSONSchema7Definition = {
|
||||
allOf: [
|
||||
{
|
||||
type: 'object',
|
||||
},
|
||||
],
|
||||
};
|
||||
if (typeof schema === 'object' && Array.isArray(schema.allOf)) {
|
||||
Object.assign(schema.allOf[0], {'x-mcp-type': 'HTMLElement'});
|
||||
}
|
||||
|
||||
replaceHtmlElementsWithUids(schema);
|
||||
|
||||
if (typeof schema === 'object' && Array.isArray(schema.allOf)) {
|
||||
const firstItem = schema.allOf[0];
|
||||
if (typeof firstItem === 'object') {
|
||||
assert.deepStrictEqual(firstItem.properties, {
|
||||
uid: {type: 'string'},
|
||||
});
|
||||
} else {
|
||||
assert.fail('First item should be an object');
|
||||
}
|
||||
} else {
|
||||
assert.fail('Unexpected schema structure');
|
||||
}
|
||||
});
|
||||
|
||||
it('recurses into oneOf', () => {
|
||||
const schema: JSONSchema7Definition = {
|
||||
oneOf: [
|
||||
{
|
||||
type: 'object',
|
||||
},
|
||||
],
|
||||
};
|
||||
if (typeof schema === 'object' && Array.isArray(schema.oneOf)) {
|
||||
Object.assign(schema.oneOf[0], {'x-mcp-type': 'HTMLElement'});
|
||||
}
|
||||
|
||||
replaceHtmlElementsWithUids(schema);
|
||||
|
||||
if (typeof schema === 'object' && Array.isArray(schema.oneOf)) {
|
||||
const firstItem = schema.oneOf[0];
|
||||
if (typeof firstItem === 'object') {
|
||||
assert.deepStrictEqual(firstItem.properties, {
|
||||
uid: {type: 'string'},
|
||||
});
|
||||
} else {
|
||||
assert.fail('First item should be an object');
|
||||
}
|
||||
} else {
|
||||
assert.fail('Unexpected schema structure');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('webmcp', () => {
|
||||
async function testIncludesWebmcpTools(
|
||||
t: it.TestContext,
|
||||
|
||||
Reference in New Issue
Block a user