import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { clickSchema, fillSchema, selectSchema, hoverSchema, focusSchema } from '../schemas.js';
import { getPageForOperation } from '../tabs.js';
import { getDefaultTimeout } from '../browser.js';
import {
handleResult,
ok,
err,
selectorNotFound,
normalizeError,
} from '../errors.js';
import type { MouseButton } from '../types.js';
/**
* Register interaction tools
*/
export function registerInteractionTools(server: McpServer): void {
// Click element
server.tool(
'click',
'Click an element on the page',
clickSchema.shape,
async ({ selector, button, clickCount, delay, timeout, tabId }) => {
const pageResult = await getPageForOperation(tabId);
if (!pageResult.success) {
return handleResult(pageResult);
}
const page = pageResult.data;
const timeoutMs = timeout ?? getDefaultTimeout();
try {
// Wait for element to be visible
const element = await page.waitForSelector(selector, {
timeout: timeoutMs,
visible: true,
});
if (!element) {
return handleResult(err(selectorNotFound(selector)));
}
await element.click({
button: (button ?? 'left') as MouseButton,
clickCount: clickCount ?? 1,
delay: delay,
});
return handleResult(ok({ clicked: true, selector }));
} catch (error) {
if (error instanceof Error && error.message.includes('waiting for selector')) {
return handleResult(err(selectorNotFound(selector)));
}
return handleResult(err(normalizeError(error)));
}
}
);
// Fill input
server.tool(
'fill',
'Fill a text input or textarea with a value',
fillSchema.shape,
async ({ selector, value, clearFirst, timeout, tabId }) => {
const pageResult = await getPageForOperation(tabId);
if (!pageResult.success) {
return handleResult(pageResult);
}
const page = pageResult.data;
const timeoutMs = timeout ?? getDefaultTimeout();
try {
const element = await page.waitForSelector(selector, {
timeout: timeoutMs,
});
if (!element) {
return handleResult(err(selectorNotFound(selector)));
}
if (clearFirst ?? true) {
// Triple-click to select all, then delete
await element.click({ clickCount: 3 });
await page.keyboard.press('Backspace');
}
await element.type(value);
return handleResult(ok({ filled: true, selector, value }));
} catch (error) {
if (error instanceof Error && error.message.includes('waiting for selector')) {
return handleResult(err(selectorNotFound(selector)));
}
return handleResult(err(normalizeError(error)));
}
}
);
// Select dropdown option
server.tool(
'select',
'Select option(s) from a dropdown/select element',
selectSchema.shape,
async ({ selector, values, timeout, tabId }) => {
const pageResult = await getPageForOperation(tabId);
if (!pageResult.success) {
return handleResult(pageResult);
}
const page = pageResult.data;
const timeoutMs = timeout ?? getDefaultTimeout();
try {
await page.waitForSelector(selector, { timeout: timeoutMs });
const selected = await page.select(selector, ...values);
return handleResult(ok({ selected, selector }));
} catch (error) {
if (error instanceof Error && error.message.includes('waiting for selector')) {
return handleResult(err(selectorNotFound(selector)));
}
return handleResult(err(normalizeError(error)));
}
}
);
// Hover over element
server.tool(
'hover',
'Hover over an element on the page',
hoverSchema.shape,
async ({ selector, timeout, tabId }) => {
const pageResult = await getPageForOperation(tabId);
if (!pageResult.success) {
return handleResult(pageResult);
}
const page = pageResult.data;
const timeoutMs = timeout ?? getDefaultTimeout();
try {
const element = await page.waitForSelector(selector, {
timeout: timeoutMs,
});
if (!element) {
return handleResult(err(selectorNotFound(selector)));
}
await element.hover();
return handleResult(ok({ hovered: true, selector }));
} catch (error) {
if (error instanceof Error && error.message.includes('waiting for selector')) {
return handleResult(err(selectorNotFound(selector)));
}
return handleResult(err(normalizeError(error)));
}
}
);
// Focus element
server.tool(
'focus',
'Focus an element on the page',
focusSchema.shape,
async ({ selector, timeout, tabId }) => {
const pageResult = await getPageForOperation(tabId);
if (!pageResult.success) {
return handleResult(pageResult);
}
const page = pageResult.data;
const timeoutMs = timeout ?? getDefaultTimeout();
try {
const element = await page.waitForSelector(selector, {
timeout: timeoutMs,
});
if (!element) {
return handleResult(err(selectorNotFound(selector)));
}
await element.focus();
return handleResult(ok({ focused: true, selector }));
} catch (error) {
if (error instanceof Error && error.message.includes('waiting for selector')) {
return handleResult(err(selectorNotFound(selector)));
}
return handleResult(err(normalizeError(error)));
}
}
);
}