Skip to main content
Glama
puppeteer.ts12.8 kB
#!/usr/bin/env node /** * Puppeteer MCP (Model Context Protocol) サーバー * * このモジュールは、Puppeteerを使用してブラウザを操作するためのMCPサーバーを実装しています。 * Webページのナビゲーション、スクリーンショット取得、要素のクリック、フォーム入力などの * 基本的なブラウザ操作をAIモデルから利用可能にします。 */ import { Server } from '@modelcontextprotocol/sdk/server/index.js' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, CallToolResult, TextContent, ImageContent, Tool, } from '@modelcontextprotocol/sdk/types.js' import puppeteer, { Browser, Page } from 'puppeteer' // ツール定義 const TOOLS: Tool[] = [ { name: 'puppeteer_navigate', description: 'Navigate to a URL', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'Target URL to navigate to' }, }, required: ['url'], }, }, { 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_select', description: 'Select an option from a select element', inputSchema: { type: 'object', properties: { selector: { type: 'string', description: 'CSS selector for select element', }, value: { type: 'string', description: 'Value to select' }, }, required: ['selector', 'value'], }, }, { name: 'puppeteer_hover', description: 'Hover over an element on the page', inputSchema: { type: 'object', properties: { selector: { type: 'string', description: 'CSS selector for element to hover', }, }, required: ['selector'], }, }, { name: 'puppeteer_evaluate', description: 'Execute JavaScript in the browser console', inputSchema: { type: 'object', properties: { script: { type: 'string', description: 'JavaScript code to execute' }, }, required: ['script'], }, }, ] // グローバル状態 let browser: Browser | undefined let page: Page | undefined const consoleLogs: string[] = [] const screenshots = new Map<string, string>() // サーバーインスタンス const server = new Server( { name: 'puppeteer-mcp', version: '0.1.0', }, { capabilities: { resources: {}, tools: {}, }, }, ) /** * グローバルなWindow型定義の拡張 */ declare global { interface Window { mcpHelper: { logs: string[] originalConsole: Partial<typeof console> } } } /** * ブラウザインスタンスが存在することを保証し、ページを返します * 初めて呼び出された場合、ブラウザを起動して設定します */ async function ensureBrowser(): Promise<Page> { if (!browser) { // ブラウザを起動 browser = await puppeteer.launch({ headless: false }) const pages = await browser.pages() page = pages[0] // コンソールログのキャプチャ設定 page.on('console', (msg) => { const logEntry = `[${msg.type()}] ${msg.text()}` consoleLogs.push(logEntry) server.notification({ method: 'notifications/resources/updated', params: { uri: 'console://logs' }, }) }) } return page! } /** * 統一されたエラーレスポンスを生成 */ function createErrorResponse(message: string): CallToolResult { return { content: [ { type: 'text', text: message, }, ], isError: true, } } /** * 統一された成功レスポンスを生成 */ function createSuccessResponse(message: string): CallToolResult { return { content: [ { type: 'text', text: message, }, ], isError: false, } } /** * URLへの移動を処理 */ async function handleNavigate(args: { url: string }): Promise<CallToolResult> { try { const page = await ensureBrowser() await page.goto(args.url) return createSuccessResponse(`Navigated to ${args.url}`) } catch (error) { return createErrorResponse( `Failed to navigate to ${args.url}: ${(error as Error).message}`, ) } } /** * スクリーンショット撮影を処理 */ async function handleScreenshot(args: { name: string selector?: string width?: number height?: number }): Promise<CallToolResult> { try { const page = await ensureBrowser() 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 createErrorResponse( args.selector ? `Element not found: ${args.selector}` : 'Screenshot failed', ) } // スクリーンショットを保存 screenshots.set(args.name, screenshot as string) server.notification({ method: 'notifications/resources/list_changed', }) return { 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, } } catch (error) { return createErrorResponse(`Screenshot failed: ${(error as Error).message}`) } } /** * 要素のクリックを処理 */ async function handleClick(args: { selector: string }): Promise<CallToolResult> { try { const page = await ensureBrowser() await page.click(args.selector) return createSuccessResponse(`Clicked: ${args.selector}`) } catch (error) { return createErrorResponse( `Failed to click ${args.selector}: ${(error as Error).message}`, ) } } /** * フォーム入力を処理 */ async function handleFill(args: { selector: string value: string }): Promise<CallToolResult> { try { const page = await ensureBrowser() await page.waitForSelector(args.selector) await page.type(args.selector, args.value) return createSuccessResponse(`Filled ${args.selector} with: ${args.value}`) } catch (error) { return createErrorResponse( `Failed to fill ${args.selector}: ${(error as Error).message}`, ) } } /** * セレクト要素の選択を処理 */ async function handleSelect(args: { selector: string value: string }): Promise<CallToolResult> { try { const page = await ensureBrowser() await page.waitForSelector(args.selector) await page.select(args.selector, args.value) return createSuccessResponse(`Selected ${args.value} in ${args.selector}`) } catch (error) { return createErrorResponse( `Failed to select ${args.selector}: ${(error as Error).message}`, ) } } /** * 要素へのホバーを処理 */ async function handleHover(args: { selector: string }): Promise<CallToolResult> { try { const page = await ensureBrowser() await page.waitForSelector(args.selector) await page.hover(args.selector) return createSuccessResponse(`Hovered ${args.selector}`) } catch (error) { return createErrorResponse( `Failed to hover ${args.selector}: ${(error as Error).message}`, ) } } /** * JavaScriptコードの実行を処理 */ async function handleEvaluate(args: { script: string }): Promise<CallToolResult> { try { const page = await ensureBrowser() // コンソールログキャプチャを設定 await page.evaluate(() => { window.mcpHelper = { logs: [], originalConsole: { ...console }, } ;['log', 'info', 'warn', 'error'].forEach((method) => { ;(console as any)[method] = (...args: any[]) => { window.mcpHelper.logs.push(`[${method}] ${args.join(' ')}`) ;(window.mcpHelper.originalConsole as any)[method](...args) } }) }) // スクリプトを実行 const result = await page.evaluate(args.script) // ログを取得して元のコンソールに戻す const logs = await page.evaluate(() => { const logs = window.mcpHelper.logs Object.assign(console, window.mcpHelper.originalConsole) delete (window as any).mcpHelper return logs }) return { content: [ { type: 'text', text: `Execution result:\n${JSON.stringify( result, null, 2, )}\n\nConsole output:\n${logs.join('\n')}`, }, ], isError: false, } } catch (error) { return createErrorResponse( `Script execution failed: ${(error as Error).message}`, ) } } /** * ツール呼び出しを処理するメインハンドラー */ async function handleToolCall( name: string, args: any, ): Promise<CallToolResult> { try { switch (name) { case 'puppeteer_navigate': return await handleNavigate(args) case 'puppeteer_screenshot': return await handleScreenshot(args) case 'puppeteer_click': return await handleClick(args) case 'puppeteer_fill': return await handleFill(args) case 'puppeteer_select': return await handleSelect(args) case 'puppeteer_hover': return await handleHover(args) case 'puppeteer_evaluate': return await handleEvaluate(args) default: return createErrorResponse(`Unknown tool: ${name}`) } } catch (error) { return createErrorResponse( `Tool execution failed: ${(error as Error).message}`, ) } } // リクエストハンドラの設定 server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [ { uri: 'console://logs', mimeType: 'text/plain', name: 'Browser console logs', }, ...Array.from(screenshots.keys()).map((name) => ({ uri: `screenshot://${name}`, mimeType: 'image/png', name: `Screenshot: ${name}`, })), ], })) server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const uri = request.params.uri.toString() if (uri === 'console://logs') { return { contents: [ { uri, mimeType: 'text/plain', text: consoleLogs.join('\n'), }, ], } } if (uri.startsWith('screenshot://')) { const name = uri.split('://')[1] const screenshot = screenshots.get(name) if (screenshot) { return { contents: [ { uri, mimeType: 'image/png', blob: screenshot, }, ], } } } throw new Error(`Resource not found: ${uri}`) }) server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS, })) server.setRequestHandler(CallToolRequestSchema, async (request) => handleToolCall(request.params.name, request.params.arguments ?? {}), ) /** * MCPサーバーを起動 */ async function runServer() { const transport = new StdioServerTransport() await server.connect(transport) } // サーバー起動 runServer().catch(console.error) // 終了処理 process.stdin.on('close', () => { console.error('Puppeteer MCP Server closed') browser?.close() server.close() })

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/drapon/claude-mcp-servers'

If you have feedback or need assistance with the MCP directory API, please join our Discord server