Update Puppeteer

This commit is contained in:
Mahesh Murag
2024-11-19 17:01:19 -05:00
parent e1cd447dbc
commit 0fdfe3feb8
2 changed files with 370 additions and 83 deletions

View File

@@ -1,3 +1,53 @@
# Puppeteer server
# Puppeteer
This MCP server provides **resources** and **tools** for interacting with a browser and web pages, using [Puppeteer](https://pptr.dev/).
A Model Context Protocol server that provides browser automation capabilities using Puppeteer. This server enables LLMs to interact with web pages, take screenshots, and execute JavaScript in a real browser environment.
## Components
### Tools
- **puppeteer_navigate**
- Navigate to any URL in the browser
- Input: `url` (string)
- **puppeteer_screenshot**
- Capture screenshots of the entire page or specific elements
- Inputs:
- `name` (string, required): Name for the screenshot
- `selector` (string, optional): CSS selector for element to screenshot
- `width` (number, optional, default: 800): Screenshot width
- `height` (number, optional, default: 600): Screenshot height
- **puppeteer_click**
- Click elements on the page
- Input: `selector` (string): CSS selector for element to click
- **puppeteer_fill**
- Fill out input fields
- Inputs:
- `selector` (string): CSS selector for input field
- `value` (string): Value to fill
- **puppeteer_evaluate**
- Execute JavaScript in the browser console
- Input: `script` (string): JavaScript code to execute
### Resources
The server provides access to two types of resources:
1. **Console Logs** (`console://logs`)
- Browser console output in text format
- Includes all console messages from the browser
2. **Screenshots** (`screenshot://<name>`)
- PNG images of captured screenshots
- Accessible via the screenshot name specified during capture
## Key Features
- Browser automation
- Console log monitoring
- Screenshot capabilities
- JavaScript execution
- Basic web interaction (navigation, clicking, form filling)

View File

@@ -7,104 +7,341 @@ import {
ListResourcesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
CallToolResult,
TextContent,
ImageContent,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import puppeteer from "puppeteer";
import puppeteer, { Browser, Page } from "puppeteer";
const server = new Server(
// Define the tools once to avoid repetition
const TOOLS: Tool[] = [
{
name: "example-servers/puppeteer",
version: "0.1.0",
},
{
capabilities: {
resources: {
listChanged: true,
name: "puppeteer_navigate",
description: "Navigate to a URL",
inputSchema: {
type: "object",
properties: {
url: { type: "string" },
},
tools: {},
required: ["url"],
},
},
);
let browser: puppeteer.Browser | undefined;
let consoleLogs: string[] = [];
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: "console://logs",
mimeType: "text/plain",
name: "Browser console logs",
{
name: "puppeteer_screenshot",
description: "Take a screenshot of the current page or a specific element",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "Name for the screenshot" },
selector: { type: "string", description: "CSS selector for element to screenshot" },
width: { type: "number", description: "Width in pixels (default: 800)" },
height: { type: "number", description: "Height in pixels (default: 600)" },
},
],
};
});
required: ["name"],
},
},
{
name: "puppeteer_click",
description: "Click an element on the page",
inputSchema: {
type: "object",
properties: {
selector: { type: "string", description: "CSS selector for element to click" },
},
required: ["selector"],
},
},
{
name: "puppeteer_fill",
description: "Fill out an input field",
inputSchema: {
type: "object",
properties: {
selector: { type: "string", description: "CSS selector for input field" },
value: { type: "string", description: "Value to fill" },
},
required: ["selector", "value"],
},
},
{
name: "puppeteer_evaluate",
description: "Execute JavaScript in the browser console",
inputSchema: {
type: "object",
properties: {
script: { type: "string", description: "JavaScript code to execute" },
},
required: ["script"],
},
},
];
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
if (request.params.uri.toString() === "console://logs") {
return {
contents: [
{
uri: "console://logs",
mimeType: "text/plain",
text: consoleLogs.join("\n"),
},
],
};
class PuppeteerServer {
private browser?: Browser;
private page?: Page;
private consoleLogs: string[] = [];
private screenshots: Map<string, string> = new Map();
private server: Server;
constructor() {
this.server = new Server({
name: "example-servers/puppeteer",
version: "0.4.0",
});
this.setupHandlers();
}
console.error("Resource not found:", request.params.uri);
throw new Error("Resource not found");
});
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "navigate",
description: "Navigate to a URL",
inputSchema: {
type: "object",
properties: {
url: { type: "string" },
},
},
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "navigate") {
const url = request.params.arguments?.url as string;
if (!browser) {
browser = await puppeteer.launch({ headless: false });
const pages = await browser.pages();
pages[0].on("console", (msg) => {
private async ensureBrowser() {
if (!this.browser) {
this.browser = await puppeteer.launch({ headless: false });
const pages = await this.browser.pages();
this.page = pages[0];
this.page.on("console", (msg) => {
const logEntry = `[${msg.type()}] ${msg.text()}`;
consoleLogs.push(logEntry);
server.notification({
this.consoleLogs.push(logEntry);
this.server.notification({
method: "notifications/resources/updated",
params: { uri: "console://logs" },
});
});
}
const pages = await browser.pages();
await pages[0].goto(url);
return {
content: [{ type: "text", text: `Navigated to ${url}` }],
isError: false,
};
return this.page!;
}
throw new Error(`Unknown tool: ${request.params.name}`);
});
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
private setupHandlers() {
this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: "console://logs",
mimeType: "text/plain",
name: "Browser console logs",
},
...Array.from(this.screenshots.keys()).map(name => ({
uri: `screenshot://${name}`,
mimeType: "image/png",
name: `Screenshot: ${name}`,
})),
],
}));
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri.toString();
if (uri === "console://logs") {
return {
contents: [{
uri,
mimeType: "text/plain",
text: this.consoleLogs.join("\n"),
}],
};
}
if (uri.startsWith("screenshot://")) {
const name = uri.split("://")[1];
const screenshot = this.screenshots.get(name);
if (screenshot) {
return {
contents: [{
uri,
mimeType: "image/png",
blob: screenshot,
}],
};
}
}
throw new Error(`Resource not found: ${uri}`);
});
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: TOOLS,
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) =>
this.handleToolCall(request.params.name, request.params.arguments ?? {})
);
}
private async handleToolCall(name: string, args: any): Promise<{ toolResult: CallToolResult }> {
const page = await this.ensureBrowser();
switch (name) {
case "puppeteer_navigate":
await page.goto(args.url);
return {
toolResult: {
content: [{
type: "text",
text: `Navigated to ${args.url}`,
}],
isError: false,
},
};
case "puppeteer_screenshot": {
const width = args.width ?? 800;
const height = args.height ?? 600;
await page.setViewport({ width, height });
const screenshot = await (args.selector ?
(await page.$(args.selector))?.screenshot({ encoding: "base64" }) :
page.screenshot({ encoding: "base64", fullPage: false }));
if (!screenshot) {
return {
toolResult: {
content: [{
type: "text",
text: args.selector ? `Element not found: ${args.selector}` : "Screenshot failed",
}],
isError: true,
},
};
}
this.screenshots.set(args.name, screenshot as string);
this.server.notification({
method: "notifications/resources/list_changed",
});
return {
toolResult: {
content: [
{
type: "text",
text: `Screenshot '${args.name}' taken at ${width}x${height}`,
} as TextContent,
{
type: "image",
data: screenshot,
mimeType: "image/png",
} as ImageContent,
],
isError: false,
},
};
}
case "puppeteer_click":
try {
await page.click(args.selector);
return {
toolResult: {
content: [{
type: "text",
text: `Clicked: ${args.selector}`,
}],
isError: false,
},
};
} catch (error) {
return {
toolResult: {
content: [{
type: "text",
text: `Failed to click ${args.selector}: ${(error as Error).message}`,
}],
isError: true,
},
};
}
case "puppeteer_fill":
try {
await page.waitForSelector(args.selector);
await page.type(args.selector, args.value);
return {
toolResult: {
content: [{
type: "text",
text: `Filled ${args.selector} with: ${args.value}`,
}],
isError: false,
},
};
} catch (error) {
return {
toolResult: {
content: [{
type: "text",
text: `Failed to fill ${args.selector}: ${(error as Error).message}`,
}],
isError: true,
},
};
}
case "puppeteer_evaluate":
try {
const result = await page.evaluate((script) => {
const logs: string[] = [];
const originalConsole = { ...console };
['log', 'info', 'warn', 'error'].forEach(method => {
(console as any)[method] = (...args: any[]) => {
logs.push(`[${method}] ${args.join(' ')}`);
(originalConsole as any)[method](...args);
};
});
try {
const result = eval(script);
Object.assign(console, originalConsole);
return { result, logs };
} catch (error) {
Object.assign(console, originalConsole);
throw error;
}
}, args.script);
return {
toolResult: {
content: [
{
type: "text",
text: `Execution result:\n${JSON.stringify(result.result, null, 2)}\n\nConsole output:\n${result.logs.join('\n')}`,
},
],
isError: false,
},
};
} catch (error) {
return {
toolResult: {
content: [{
type: "text",
text: `Script execution failed: ${(error as Error).message}`,
}],
isError: true,
},
};
}
default:
return {
toolResult: {
content: [{
type: "text",
text: `Unknown tool: ${name}`,
}],
isError: true,
},
};
}
}
async start() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
}
}
runServer().catch(console.error);
async function runServer() {
const server = new PuppeteerServer();
await server.start();
}
runServer().catch(console.error);