import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { evaluateSchema, getContentSchema, querySelectorSchema } from '../schemas.js';
import { getPageForOperation } from '../tabs.js';
import {
handleResult,
ok,
err,
evaluationError,
selectorNotFound,
normalizeError,
} from '../errors.js';
/**
* Register content extraction tools
*/
export function registerContentTools(server: McpServer): void {
// Evaluate JavaScript
server.tool(
'evaluate',
'Execute JavaScript code in the browser context and return the result',
evaluateSchema.shape,
async ({ script, tabId }) => {
const pageResult = await getPageForOperation(tabId);
if (!pageResult.success) {
return handleResult(pageResult);
}
const page = pageResult.data;
try {
// Use Function constructor to evaluate the script
const result = await page.evaluate((code) => {
// eslint-disable-next-line no-new-func
const fn = new Function(code);
return fn();
}, script);
return handleResult(ok({ result }));
} catch (error) {
if (error instanceof Error) {
return handleResult(err(evaluationError(error.message)));
}
return handleResult(err(normalizeError(error)));
}
}
);
// Get page content
server.tool(
'get_content',
'Get the HTML or text content of the page or a specific element',
getContentSchema.shape,
async ({ selector, type, tabId }) => {
const pageResult = await getPageForOperation(tabId);
if (!pageResult.success) {
return handleResult(pageResult);
}
const page = pageResult.data;
const contentType = type ?? 'text';
try {
if (selector) {
// Get content of specific element
const element = await page.$(selector);
if (!element) {
return handleResult(err(selectorNotFound(selector)));
}
const content = await element.evaluate((el, t) => {
return t === 'html' ? el.innerHTML : el.textContent ?? '';
}, contentType);
return handleResult(ok({ content, selector }));
} else {
// Get full page content
let content: string;
if (contentType === 'html') {
content = await page.content();
} else {
content = await page.evaluate(() => document.body.innerText);
}
return handleResult(ok({ content }));
}
} catch (error) {
return handleResult(err(normalizeError(error)));
}
}
);
// Query selector
server.tool(
'query_selector',
'Get information about an element matching a CSS selector',
querySelectorSchema.shape,
async ({ selector, tabId }) => {
const pageResult = await getPageForOperation(tabId);
if (!pageResult.success) {
return handleResult(pageResult);
}
const page = pageResult.data;
try {
const element = await page.$(selector);
if (!element) {
return handleResult(ok({
exists: false,
selector,
}));
}
const info = await element.evaluate((el) => {
const rect = el.getBoundingClientRect();
const attributes: Record<string, string> = {};
for (let i = 0; i < el.attributes.length; i++) {
const attr = el.attributes[i];
if (attr) {
attributes[attr.name] = attr.value;
}
}
return {
tagName: el.tagName.toLowerCase(),
textContent: el.textContent?.slice(0, 1000) ?? '',
attributes,
boundingBox: {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
},
};
});
return handleResult(ok({
exists: true,
selector,
...info,
}));
} catch (error) {
return handleResult(err(normalizeError(error)));
}
}
);
}