#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { chromium, type Browser } from "playwright"; const server = new McpServer({ name: "{{PROJECT_NAME}}", version: "{{VERSION}}", }); // ── Browser management ─────────────────────────────────── // Reuse a single browser instance across tool calls for performance. let browser: Browser | null = null; async function getBrowser(): Promise { if (!browser || !browser.isConnected()) { browser = await chromium.launch({ headless: true }); } return browser; } async function withPage( url: string, fn: (page: import("playwright").Page) => Promise, options: { timeoutMs?: number } = {} ): Promise { const { timeoutMs = 30000 } = options; const b = await getBrowser(); const context = await b.newContext({ userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", }); const page = await context.newPage(); page.setDefaultTimeout(timeoutMs); try { await page.goto(url, { waitUntil: "networkidle" }); return await fn(page); } finally { await context.close(); } } // ── Example: Scrape page content ───────────────────────── // This template shows how to build a Playwright-based MCP server. // Use this for sites that require JavaScript rendering. server.tool( "scrape_page", "Load a page in a real browser and extract text content, title, and metadata", { url: z.string().url().describe("URL to scrape"), selector: z .string() .optional() .describe("CSS selector to extract (defaults to body)"), }, async ({ url, selector }) => { try { const result = await withPage(url, async (page) => { const title = await page.title(); const target = selector || "body"; const text = await page .locator(target) .first() .innerText() .catch(() => "Element not found"); const meta: Record = {}; const metaTags = await page.locator("meta[name], meta[property]").all(); for (const tag of metaTags.slice(0, 20)) { const name = (await tag.getAttribute("name")) || (await tag.getAttribute("property")) || ""; const content = (await tag.getAttribute("content")) || ""; if (name && content) meta[name] = content.slice(0, 200); } return { url, title, text: text.slice(0, 5000), meta }; }); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } catch (e: any) { return { content: [{ type: "text", text: `Error scraping ${url}: ${e.message}` }], }; } } ); server.tool( "screenshot", "Take a screenshot of a webpage", { url: z.string().url().describe("URL to screenshot"), full_page: z .boolean() .optional() .default(false) .describe("Capture full page or just viewport"), }, async ({ url, full_page }) => { try { const buffer = await withPage(url, async (page) => { return await page.screenshot({ fullPage: full_page }); }); return { content: [ { type: "image", data: buffer.toString("base64"), mimeType: "image/png", }, ], }; } catch (e: any) { return { content: [ { type: "text", text: `Error taking screenshot of ${url}: ${e.message}` }, ], }; } } ); // ── Cleanup on exit ────────────────────────────────────── process.on("SIGINT", async () => { if (browser) await browser.close(); process.exit(0); }); // ── Start server ───────────────────────────────────────── async function main() { const transport = new StdioServerTransport(); await server.connect(transport); } main().catch(console.error);