refactor: extract ToolHandler (#2032)
Follow-up for https://github.com/ChromeDevTools/chrome-devtools-mcp/pull/2030
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {parseArguments} from './bin/chrome-devtools-mcp-cli-options.js';
|
||||
import {logger} from './logger.js';
|
||||
import type {McpContext} from './McpContext.js';
|
||||
import {McpResponse} from './McpResponse.js';
|
||||
import type {Mutex} from './Mutex.js';
|
||||
import {SlimMcpResponse} from './SlimMcpResponse.js';
|
||||
import {ClearcutLogger} from './telemetry/ClearcutLogger.js';
|
||||
import {bucketizeLatency} from './telemetry/metricUtils.js';
|
||||
import type {CallToolResult, zod} from './third_party/index.js';
|
||||
import type {ToolCategory} from './tools/categories.js';
|
||||
import {labels, OFF_BY_DEFAULT_CATEGORIES} from './tools/categories.js';
|
||||
import type {DefinedPageTool, ToolDefinition} from './tools/ToolDefinition.js';
|
||||
import {pageIdSchema} from './tools/ToolDefinition.js';
|
||||
|
||||
export function buildFlag(category: ToolCategory) {
|
||||
return `category${category.charAt(0).toUpperCase() + category.slice(1)}`;
|
||||
}
|
||||
|
||||
function buildDisabledMessage(
|
||||
toolName: string,
|
||||
flag: string,
|
||||
categoryLabel?: string,
|
||||
): string {
|
||||
const reason = categoryLabel
|
||||
? `is in category ${categoryLabel} which`
|
||||
: `requires experimental feature ${flag} and`;
|
||||
|
||||
return `Tool ${toolName} ${reason} is currently disabled. Enable it by running chrome-devtools start ${flag}=true. For more information check the README.`;
|
||||
}
|
||||
|
||||
function getCategoryStatus(
|
||||
category: ToolCategory,
|
||||
serverArgs: ReturnType<typeof parseArguments>,
|
||||
): {categoryFlag?: string; disabled: boolean} {
|
||||
const categoryFlag = buildFlag(category);
|
||||
|
||||
const flagValue = serverArgs[categoryFlag];
|
||||
|
||||
const isDisabled = OFF_BY_DEFAULT_CATEGORIES.includes(category)
|
||||
? !flagValue
|
||||
: flagValue === false;
|
||||
|
||||
if (isDisabled) {
|
||||
return {
|
||||
categoryFlag,
|
||||
disabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
};
|
||||
}
|
||||
|
||||
function getConditionStatus(
|
||||
condition: string,
|
||||
serverArgs: ReturnType<typeof parseArguments>,
|
||||
): {conditionFlag?: string; disabled: boolean} {
|
||||
if (condition && !serverArgs[condition]) {
|
||||
return {conditionFlag: condition, disabled: true};
|
||||
}
|
||||
|
||||
return {disabled: false};
|
||||
}
|
||||
|
||||
function getToolStatusInfo(
|
||||
tool: ToolDefinition | DefinedPageTool,
|
||||
serverArgs: ReturnType<typeof parseArguments>,
|
||||
): {disabled: boolean; reason?: string} {
|
||||
const category = tool.annotations.category;
|
||||
const categoryCheck = getCategoryStatus(category, serverArgs);
|
||||
|
||||
if (category && categoryCheck.disabled) {
|
||||
if (!categoryCheck.categoryFlag) {
|
||||
throw new Error(
|
||||
'when the category is disabled there should always be a flag set',
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
disabled: true,
|
||||
reason: buildDisabledMessage(
|
||||
tool.name,
|
||||
`--${categoryCheck.categoryFlag}`,
|
||||
labels[category!],
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
for (const condition of tool.annotations.conditions || []) {
|
||||
const conditionCheck = getConditionStatus(condition, serverArgs);
|
||||
if (conditionCheck.disabled) {
|
||||
if (!conditionCheck.conditionFlag) {
|
||||
throw new Error(
|
||||
'when the condition is disabled there should always be a flag set',
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
disabled: true,
|
||||
reason: buildDisabledMessage(
|
||||
tool.name,
|
||||
`--${conditionCheck.conditionFlag}`,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {disabled: false};
|
||||
}
|
||||
|
||||
function isPageScopedTool(
|
||||
tool: ToolDefinition | DefinedPageTool,
|
||||
): tool is DefinedPageTool {
|
||||
return 'pageScoped' in tool && tool.pageScoped === true;
|
||||
}
|
||||
|
||||
export class ToolHandler {
|
||||
readonly inputSchema: zod.ZodRawShape;
|
||||
readonly shouldRegister: boolean;
|
||||
private readonly disabledReason?: string;
|
||||
|
||||
constructor(
|
||||
private readonly tool: ToolDefinition | DefinedPageTool,
|
||||
private readonly serverArgs: ReturnType<typeof parseArguments>,
|
||||
private readonly getContext: () => Promise<McpContext>,
|
||||
private readonly toolMutex: Mutex,
|
||||
) {
|
||||
const {disabled, reason} = getToolStatusInfo(tool, serverArgs);
|
||||
this.disabledReason = reason;
|
||||
this.shouldRegister = !(disabled && !serverArgs.viaCli);
|
||||
|
||||
this.inputSchema =
|
||||
'pageScoped' in tool &&
|
||||
tool.pageScoped &&
|
||||
serverArgs.experimentalPageIdRouting &&
|
||||
!serverArgs.slim
|
||||
? {...tool.schema, ...pageIdSchema}
|
||||
: tool.schema;
|
||||
}
|
||||
|
||||
async handle(params: Record<string, unknown>): Promise<CallToolResult> {
|
||||
if (this.disabledReason) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: this.disabledReason,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const guard = await this.toolMutex.acquire();
|
||||
const startTime = Date.now();
|
||||
let success = false;
|
||||
try {
|
||||
logger(
|
||||
`${this.tool.name} request: ${JSON.stringify(params, null, ' ')}`,
|
||||
);
|
||||
const context = await this.getContext();
|
||||
logger(`${this.tool.name} context: resolved`);
|
||||
await context.detectOpenDevToolsWindows();
|
||||
const response = this.serverArgs.slim
|
||||
? new SlimMcpResponse(this.serverArgs)
|
||||
: new McpResponse(this.serverArgs);
|
||||
|
||||
response.setRedactNetworkHeaders(this.serverArgs.redactNetworkHeaders);
|
||||
try {
|
||||
if (isPageScopedTool(this.tool)) {
|
||||
const pageId =
|
||||
typeof params.pageId === 'number' ? params.pageId : undefined;
|
||||
const page =
|
||||
this.serverArgs.experimentalPageIdRouting &&
|
||||
pageId !== undefined &&
|
||||
!this.serverArgs.slim
|
||||
? context.getPageById(pageId)
|
||||
: context.getSelectedMcpPage();
|
||||
response.setPage(page);
|
||||
if (this.tool.blockedByDialog) {
|
||||
page.throwIfDialogOpen();
|
||||
}
|
||||
await this.tool.handler(
|
||||
{
|
||||
params,
|
||||
page,
|
||||
},
|
||||
response,
|
||||
context,
|
||||
);
|
||||
} else {
|
||||
await this.tool.handler(
|
||||
{
|
||||
params,
|
||||
},
|
||||
response,
|
||||
context,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
response.setError(err);
|
||||
}
|
||||
const {content, structuredContent} = await response.handle(
|
||||
this.tool.name,
|
||||
context,
|
||||
);
|
||||
const result: CallToolResult & {
|
||||
structuredContent?: Record<string, unknown>;
|
||||
} = {
|
||||
content,
|
||||
};
|
||||
if (response.error) {
|
||||
result.isError = true;
|
||||
}
|
||||
success = true;
|
||||
if (this.serverArgs.experimentalStructuredContent) {
|
||||
result.structuredContent = structuredContent as Record<string, unknown>;
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
logger(`${this.tool.name} error:`, err, err?.stack);
|
||||
let errorText = err && 'message' in err ? err.message : String(err);
|
||||
if ('cause' in err && err.cause) {
|
||||
errorText += `\nCause: ${err.cause.message}`;
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: errorText,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
} finally {
|
||||
void ClearcutLogger.get()?.logToolInvocation({
|
||||
toolName: this.tool.name,
|
||||
params,
|
||||
schema: this.inputSchema,
|
||||
success,
|
||||
latencyMs: bucketizeLatency(Date.now() - startTime),
|
||||
});
|
||||
guard.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-216
@@ -12,11 +12,8 @@ import {ensureBrowserConnected, ensureBrowserLaunched} from './browser.js';
|
||||
import {loadIssueDescriptions} from './issue-descriptions.js';
|
||||
import {logger} from './logger.js';
|
||||
import {McpContext} from './McpContext.js';
|
||||
import {McpResponse} from './McpResponse.js';
|
||||
import {Mutex} from './Mutex.js';
|
||||
import {SlimMcpResponse} from './SlimMcpResponse.js';
|
||||
import {ClearcutLogger} from './telemetry/ClearcutLogger.js';
|
||||
import {bucketizeLatency} from './telemetry/metricUtils.js';
|
||||
import {
|
||||
McpServer,
|
||||
type CallToolResult,
|
||||
@@ -24,109 +21,12 @@ import {
|
||||
ListRootsResultSchema,
|
||||
RootsListChangedNotificationSchema,
|
||||
} from './third_party/index.js';
|
||||
import type {ToolCategory} from './tools/categories.js';
|
||||
import {labels, OFF_BY_DEFAULT_CATEGORIES} from './tools/categories.js';
|
||||
import {ToolHandler} from './ToolHandler.js';
|
||||
import type {DefinedPageTool, ToolDefinition} from './tools/ToolDefinition.js';
|
||||
import {pageIdSchema} from './tools/ToolDefinition.js';
|
||||
import {createTools} from './tools/tools.js';
|
||||
import {VERSION} from './version.js';
|
||||
|
||||
export function buildFlag(category: ToolCategory) {
|
||||
return `category${category.charAt(0).toUpperCase() + category.slice(1)}`;
|
||||
}
|
||||
|
||||
function buildDisabledMessage(
|
||||
toolName: string,
|
||||
flag: string,
|
||||
categoryLabel?: string,
|
||||
): string {
|
||||
const reason = categoryLabel
|
||||
? `is in category ${categoryLabel} which`
|
||||
: `requires experimental feature ${flag} and`;
|
||||
|
||||
return `Tool ${toolName} ${reason} is currently disabled. Enable it by running chrome-devtools start ${flag}=true. For more information check the README.`;
|
||||
}
|
||||
|
||||
function getCategoryStatus(
|
||||
category: ToolCategory,
|
||||
serverArgs: ReturnType<typeof parseArguments>,
|
||||
): {categoryFlag?: string; disabled: boolean} {
|
||||
const categoryFlag = buildFlag(category);
|
||||
|
||||
const flagValue = serverArgs[categoryFlag];
|
||||
|
||||
const isDisabled = OFF_BY_DEFAULT_CATEGORIES.includes(category)
|
||||
? !flagValue
|
||||
: flagValue === false;
|
||||
|
||||
if (isDisabled) {
|
||||
return {
|
||||
categoryFlag,
|
||||
disabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
};
|
||||
}
|
||||
|
||||
function getConditionStatus(
|
||||
condition: string,
|
||||
serverArgs: ReturnType<typeof parseArguments>,
|
||||
): {conditionFlag?: string; disabled: boolean} {
|
||||
if (condition && !serverArgs[condition]) {
|
||||
return {conditionFlag: condition, disabled: true};
|
||||
}
|
||||
|
||||
return {disabled: false};
|
||||
}
|
||||
|
||||
function getToolStatusInfo(
|
||||
tool: ToolDefinition | DefinedPageTool,
|
||||
serverArgs: ReturnType<typeof parseArguments>,
|
||||
): {disabled: boolean; reason?: string} {
|
||||
const category = tool.annotations.category;
|
||||
const categoryCheck = getCategoryStatus(category, serverArgs);
|
||||
|
||||
if (category && categoryCheck.disabled) {
|
||||
if (!categoryCheck.categoryFlag) {
|
||||
throw new Error(
|
||||
'when the category is disabled there should always be a flag set',
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
disabled: true,
|
||||
reason: buildDisabledMessage(
|
||||
tool.name,
|
||||
`--${categoryCheck.categoryFlag}`,
|
||||
labels[category!],
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
for (const condition of tool.annotations.conditions || []) {
|
||||
const conditionCheck = getConditionStatus(condition, serverArgs);
|
||||
if (conditionCheck.disabled) {
|
||||
if (!conditionCheck.conditionFlag) {
|
||||
throw new Error(
|
||||
'when the condition is disabled there should always be a flag set',
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
disabled: true,
|
||||
reason: buildDisabledMessage(
|
||||
tool.name,
|
||||
`--${conditionCheck.conditionFlag}`,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {disabled: false};
|
||||
}
|
||||
export {buildFlag} from './ToolHandler.js';
|
||||
|
||||
export async function createMcpServer(
|
||||
serverArgs: ReturnType<typeof parseArguments>,
|
||||
@@ -240,134 +140,26 @@ export async function createMcpServer(
|
||||
const toolMutex = new Mutex();
|
||||
|
||||
function registerTool(tool: ToolDefinition | DefinedPageTool): void {
|
||||
const {disabled, reason: disabledReason} = getToolStatusInfo(
|
||||
const toolHandler = new ToolHandler(
|
||||
tool,
|
||||
serverArgs,
|
||||
getContext,
|
||||
toolMutex,
|
||||
);
|
||||
|
||||
if (disabled && !serverArgs.viaCli) {
|
||||
if (!toolHandler.shouldRegister) {
|
||||
return;
|
||||
}
|
||||
|
||||
const schema =
|
||||
'pageScoped' in tool &&
|
||||
tool.pageScoped &&
|
||||
serverArgs.experimentalPageIdRouting &&
|
||||
!serverArgs.slim
|
||||
? {...tool.schema, ...pageIdSchema}
|
||||
: tool.schema;
|
||||
|
||||
server.registerTool(
|
||||
tool.name,
|
||||
{
|
||||
description: tool.description,
|
||||
inputSchema: schema,
|
||||
inputSchema: toolHandler.inputSchema,
|
||||
annotations: tool.annotations,
|
||||
},
|
||||
async (params): Promise<CallToolResult> => {
|
||||
if (disabledReason) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: disabledReason,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const guard = await toolMutex.acquire();
|
||||
const startTime = Date.now();
|
||||
let success = false;
|
||||
try {
|
||||
logger(`${tool.name} request: ${JSON.stringify(params, null, ' ')}`);
|
||||
const context = await getContext();
|
||||
logger(`${tool.name} context: resolved`);
|
||||
await context.detectOpenDevToolsWindows();
|
||||
const response = serverArgs.slim
|
||||
? new SlimMcpResponse(serverArgs)
|
||||
: new McpResponse(serverArgs);
|
||||
|
||||
response.setRedactNetworkHeaders(serverArgs.redactNetworkHeaders);
|
||||
try {
|
||||
if ('pageScoped' in tool && tool.pageScoped) {
|
||||
const page =
|
||||
serverArgs.experimentalPageIdRouting &&
|
||||
params.pageId &&
|
||||
!serverArgs.slim
|
||||
? context.getPageById(params.pageId)
|
||||
: context.getSelectedMcpPage();
|
||||
response.setPage(page);
|
||||
if (tool.blockedByDialog) {
|
||||
page.throwIfDialogOpen();
|
||||
}
|
||||
await tool.handler(
|
||||
{
|
||||
params,
|
||||
page,
|
||||
},
|
||||
response,
|
||||
context,
|
||||
);
|
||||
} else {
|
||||
await tool.handler(
|
||||
// @ts-expect-error types do not match.
|
||||
{
|
||||
params,
|
||||
},
|
||||
response,
|
||||
context,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
response.setError(err);
|
||||
}
|
||||
const {content, structuredContent} = await response.handle(
|
||||
tool.name,
|
||||
context,
|
||||
);
|
||||
const result: CallToolResult & {
|
||||
structuredContent?: Record<string, unknown>;
|
||||
} = {
|
||||
content,
|
||||
};
|
||||
if (response.error) {
|
||||
result.isError = true;
|
||||
}
|
||||
success = true;
|
||||
if (serverArgs.experimentalStructuredContent) {
|
||||
result.structuredContent = structuredContent as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
logger(`${tool.name} error:`, err, err?.stack);
|
||||
let errorText = err && 'message' in err ? err.message : String(err);
|
||||
if ('cause' in err && err.cause) {
|
||||
errorText += `\nCause: ${err.cause.message}`;
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: errorText,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
} finally {
|
||||
void ClearcutLogger.get()?.logToolInvocation({
|
||||
toolName: tool.name,
|
||||
params,
|
||||
schema,
|
||||
success,
|
||||
latencyMs: bucketizeLatency(Date.now() - startTime),
|
||||
});
|
||||
guard.dispose();
|
||||
}
|
||||
return await toolHandler.handle(params);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import assert from 'node:assert';
|
||||
import {afterEach, describe, it} from 'node:test';
|
||||
|
||||
import sinon from 'sinon';
|
||||
|
||||
import {parseArguments} from '../src/bin/chrome-devtools-mcp-cli-options.js';
|
||||
import {McpContext} from '../src/McpContext.js';
|
||||
import {McpPage} from '../src/McpPage.js';
|
||||
import {Mutex} from '../src/Mutex.js';
|
||||
import {ToolHandler} from '../src/ToolHandler.js';
|
||||
import {ToolCategory} from '../src/tools/categories.js';
|
||||
import type {
|
||||
DefinedPageTool,
|
||||
ToolDefinition,
|
||||
} from '../src/tools/ToolDefinition.js';
|
||||
|
||||
describe('ToolHandler', () => {
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('calls page getter for page scoped tools', async () => {
|
||||
let handlerCalled = false;
|
||||
const tool: DefinedPageTool = {
|
||||
name: 'page_tool',
|
||||
description: 'A page scoped tool',
|
||||
annotations: {
|
||||
category: ToolCategory.INPUT,
|
||||
readOnlyHint: false,
|
||||
},
|
||||
schema: {},
|
||||
blockedByDialog: false,
|
||||
pageScoped: true,
|
||||
handler: async () => {
|
||||
handlerCalled = true;
|
||||
},
|
||||
};
|
||||
|
||||
const mockContext = sinon.createStubInstance(McpContext);
|
||||
const mockPage = sinon.createStubInstance(McpPage);
|
||||
mockContext.getSelectedMcpPage.returns(mockPage);
|
||||
mockContext.detectOpenDevToolsWindows.resolves();
|
||||
|
||||
const toolMutex = new Mutex();
|
||||
const serverArgs = parseArguments('1.0.0', ['node', 'script.js'], {
|
||||
CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: 'true',
|
||||
});
|
||||
|
||||
const toolHandler = new ToolHandler(
|
||||
tool,
|
||||
serverArgs,
|
||||
async () => mockContext,
|
||||
toolMutex,
|
||||
);
|
||||
|
||||
assert.strictEqual(toolHandler.shouldRegister, true);
|
||||
await toolHandler.handle({});
|
||||
|
||||
assert.strictEqual(mockContext.getSelectedMcpPage.calledOnce, true);
|
||||
assert.strictEqual(handlerCalled, true);
|
||||
});
|
||||
|
||||
it('does not call page getter for non-page scoped tools', async () => {
|
||||
let handlerCalled = false;
|
||||
const tool: ToolDefinition = {
|
||||
name: 'global_tool',
|
||||
description: 'A global tool',
|
||||
annotations: {
|
||||
category: ToolCategory.NAVIGATION,
|
||||
readOnlyHint: true,
|
||||
},
|
||||
schema: {},
|
||||
blockedByDialog: false,
|
||||
handler: async () => {
|
||||
handlerCalled = true;
|
||||
},
|
||||
};
|
||||
|
||||
const mockContext = sinon.createStubInstance(McpContext);
|
||||
mockContext.detectOpenDevToolsWindows.resolves();
|
||||
|
||||
const toolMutex = new Mutex();
|
||||
const serverArgs = parseArguments('1.0.0', ['node', 'script.js'], {
|
||||
CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: 'true',
|
||||
});
|
||||
|
||||
const toolHandler = new ToolHandler(
|
||||
tool,
|
||||
serverArgs,
|
||||
async () => mockContext,
|
||||
toolMutex,
|
||||
);
|
||||
|
||||
assert.strictEqual(toolHandler.shouldRegister, true);
|
||||
const result = await toolHandler.handle({});
|
||||
|
||||
assert.strictEqual(mockContext.getSelectedMcpPage.called, false);
|
||||
assert.strictEqual(mockContext.getPageById.called, false);
|
||||
assert.strictEqual(handlerCalled, true);
|
||||
assert.strictEqual(result.isError, undefined);
|
||||
});
|
||||
|
||||
it('sets shouldRegister to false and returns disabled reason when category is disabled', async () => {
|
||||
let handlerCalled = false;
|
||||
const tool: ToolDefinition = {
|
||||
name: 'disabled_tool',
|
||||
description: 'A disabled tool',
|
||||
annotations: {
|
||||
category: ToolCategory.EMULATION,
|
||||
readOnlyHint: true,
|
||||
},
|
||||
schema: {},
|
||||
blockedByDialog: false,
|
||||
handler: async () => {
|
||||
handlerCalled = true;
|
||||
},
|
||||
};
|
||||
|
||||
const mockContext = sinon.createStubInstance(McpContext);
|
||||
const toolMutex = new Mutex();
|
||||
const serverArgs = parseArguments(
|
||||
'1.0.0',
|
||||
['node', 'script.js', '--categoryEmulation=false'],
|
||||
{CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: 'true'},
|
||||
);
|
||||
|
||||
const toolHandler = new ToolHandler(
|
||||
tool,
|
||||
serverArgs,
|
||||
async () => mockContext,
|
||||
toolMutex,
|
||||
);
|
||||
|
||||
assert.strictEqual(toolHandler.shouldRegister, false);
|
||||
|
||||
const result = await toolHandler.handle({});
|
||||
assert.strictEqual(result.isError, true);
|
||||
assert.match(
|
||||
result.content[0].type === 'text' ? result.content[0].text : '',
|
||||
/is currently disabled/,
|
||||
);
|
||||
assert.strictEqual(handlerCalled, false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user