From ab03140803c28862581bde200e59497cae418377 Mon Sep 17 00:00:00 2001 From: Adam Ladachowski Date: Sun, 8 Feb 2026 03:05:28 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=AC=20Commit=20message:=20Update=20202?= =?UTF-8?q?6-02-08=2003:05:28,=2017=20files,=20662=20lines?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 📁 Files changed: 17 📝 Lines changed: 662 • plugin.json • .mcp.json • README.md • analyze.md • compare.md • end.md • extract.md • fill.md • goto.md • restore.md • save.md • scrape.md • screenshot.md • start.md • package.json • browser.ts • mcp.ts --- .claude-plugin/plugin.json | 22 ++ .mcp.json | 5 +- README.md | 44 +++- commands/analyze.md | 13 ++ commands/compare.md | 13 ++ commands/end.md | 12 + commands/extract.md | 13 ++ commands/fill.md | 13 ++ commands/goto.md | 12 + commands/restore.md | 13 ++ commands/save.md | 15 ++ commands/scrape.md | 14 ++ commands/screenshot.md | 9 + commands/start.md | 11 + package.json | 2 +- src/browser.ts | 10 + src/mcp.ts | 441 ++++++++++++++++++++++++++++++++++++- 17 files changed, 655 insertions(+), 7 deletions(-) create mode 100644 .claude-plugin/plugin.json create mode 100644 commands/analyze.md create mode 100644 commands/compare.md create mode 100644 commands/end.md create mode 100644 commands/extract.md create mode 100644 commands/fill.md create mode 100644 commands/goto.md create mode 100644 commands/restore.md create mode 100644 commands/save.md create mode 100644 commands/scrape.md create mode 100644 commands/screenshot.md create mode 100644 commands/start.md diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..cd1506a --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,22 @@ +{ + "name": "browse", + "description": "Browser automation and image processing tools for Claude Code using Playwright WebKit", + "version": "0.2.0", + "author": { + "name": "aladac", + "url": "https://github.com/aladac" + }, + "repository": "https://github.com/aladac/claude-browse", + "license": "MIT", + "keywords": [ + "browser", + "automation", + "playwright", + "webkit", + "screenshot", + "scraping", + "image-processing" + ], + "mcpServers": "./.mcp.json", + "commands": "./commands" +} diff --git a/.mcp.json b/.mcp.json index 129b113..4ee5a14 100644 --- a/.mcp.json +++ b/.mcp.json @@ -1,8 +1,9 @@ { "mcpServers": { "claude-browse": { - "command": "npx", - "args": ["claude-browse-mcp"] + "command": "node", + "args": ["${CLAUDE_PLUGIN_ROOT}/dist/mcp.js"], + "env": {} } } } diff --git a/README.md b/README.md index 8f38362..cec43cb 100644 --- a/README.md +++ b/README.md @@ -85,9 +85,45 @@ curl -X POST localhost:3000 -d '{"cmd":"wait","ms":2000}' curl -X POST localhost:3000 -d '{"cmd":"close"}' ``` -## MCP Server (Model Context Protocol) +## Claude Code Plugin (Recommended) -Use with Claude Code or any MCP-compatible client: +Install as a Claude Code plugin for the best integration: + +```bash +# Install the plugin +claude plugin install https://github.com/aladac/claude-browse + +# Or load temporarily during development +claude --plugin-dir /path/to/claude-browse +``` + +### Plugin Features + +**Slash Commands:** + +| Command | Description | +|---------|-------------| +| `/browse:start` | Start an interactive browsing session | +| `/browse:goto ` | Navigate to URL and describe findings | +| `/browse:screenshot` | Take a screenshot of the current page | +| `/browse:scrape ` | Scrape content from a webpage | +| `/browse:analyze` | Analyze current page content and structure | +| `/browse:extract [selector]` | Extract structured data from page | +| `/browse:fill [data]` | Help fill out forms | +| `/browse:compare [action]` | Compare page states before/after action | + +**MCP Resources (@ mentions):** + +| Resource | Description | +|----------|-------------| +| `@claude-browse:browser://state` | Browser state (URL, title, launched) | +| `@claude-browse:browser://html` | Page HTML (truncated to 10KB) | +| `@claude-browse:browser://html/full` | Complete page HTML | +| `@claude-browse:browser://screenshot` | Page screenshot as base64 PNG | + +## MCP Server (Standalone) + +Use with any MCP-compatible client: ```bash # Run the MCP server @@ -106,7 +142,9 @@ Add to Claude Code's MCP config (`~/.claude/settings.json`): } ``` -Available tools: `goto`, `click`, `type`, `query`, `screenshot`, `url`, `html`, `back`, `forward`, `reload`, `wait`, `eval` +**Available Tools:** `goto`, `click`, `type`, `query`, `screenshot`, `url`, `html`, `back`, `forward`, `reload`, `wait`, `eval` + +**Image Processing Tools:** `favicon`, `convert`, `resize`, `crop`, `compress`, `thumbnail` ## Programmatic Usage diff --git a/commands/analyze.md b/commands/analyze.md new file mode 100644 index 0000000..8ea8340 --- /dev/null +++ b/commands/analyze.md @@ -0,0 +1,13 @@ +--- +description: Analyze the current page content and structure +--- + +Use the analyze_page MCP prompt to analyze the current browser page. + +If the browser is not launched, first navigate to a URL using the goto tool, then analyze the page structure, content, and interactive elements. + +Provide: +1. A summary of the page purpose +2. Key interactive elements (forms, buttons, links) +3. Notable structure or patterns +4. Suggestions for useful actions diff --git a/commands/compare.md b/commands/compare.md new file mode 100644 index 0000000..599ec10 --- /dev/null +++ b/commands/compare.md @@ -0,0 +1,13 @@ +--- +description: Compare page states with screenshots before and after an action +--- + +Compare page states: + +1. Take an initial screenshot of the current page +2. Ask what action to perform (click, navigate, submit, etc.) +3. Perform the requested action +4. Take another screenshot +5. Describe the visual and structural differences + +What action would you like to perform between screenshots? $ARGUMENTS diff --git a/commands/end.md b/commands/end.md new file mode 100644 index 0000000..8645c66 --- /dev/null +++ b/commands/end.md @@ -0,0 +1,12 @@ +--- +description: End the current browsing session and close the browser +--- + +End the current browsing session. + +Use the `close` tool to close the browser. This will: +1. Close all browser pages +2. Clear the browser state +3. Free up system resources + +The browser can be relaunched with any navigation command (goto, start, etc.) diff --git a/commands/extract.md b/commands/extract.md new file mode 100644 index 0000000..bc24281 --- /dev/null +++ b/commands/extract.md @@ -0,0 +1,13 @@ +--- +description: Extract structured data from the current page +--- + +Extract structured data from the current browser page. + +Selector (optional): $ARGUMENTS + +Steps: +1. If a selector is provided, use the query tool to find matching elements +2. Analyze the page structure to identify data patterns +3. Extract and structure the data in a useful format (JSON, table, etc.) +4. Identify patterns that could help with similar pages diff --git a/commands/fill.md b/commands/fill.md new file mode 100644 index 0000000..cf9c285 --- /dev/null +++ b/commands/fill.md @@ -0,0 +1,13 @@ +--- +description: Help fill out a form on the current page +--- + +Fill form with data: $ARGUMENTS + +Steps: +1. Use the query tool to find form inputs (input, textarea, select) +2. Identify required fields and their types +3. If data is provided, parse it and fill the matching fields using the type tool +4. Report what was filled and any issues encountered + +If no data provided, describe the form fields found and ask what to fill in. diff --git a/commands/goto.md b/commands/goto.md new file mode 100644 index 0000000..2b884ce --- /dev/null +++ b/commands/goto.md @@ -0,0 +1,12 @@ +--- +description: Navigate to a URL and describe what you find +--- + +Navigate to: $ARGUMENTS + +Steps: +1. Use the goto tool to navigate to the URL +2. Take a screenshot to see the page +3. Describe what you see +4. Identify the main interactive elements +5. Suggest what actions might be useful diff --git a/commands/restore.md b/commands/restore.md new file mode 100644 index 0000000..d615eed --- /dev/null +++ b/commands/restore.md @@ -0,0 +1,13 @@ +--- +description: Restore a previously saved browsing session +--- + +Restore session from: $ARGUMENTS + +Use the `session_restore` tool to restore a previously saved session: +- Navigate to the saved URL +- Restore all cookies +- Restore localStorage data +- Restore sessionStorage data + +If no path is specified, restore from `session.json` in the current directory. diff --git a/commands/save.md b/commands/save.md new file mode 100644 index 0000000..afcb9b9 --- /dev/null +++ b/commands/save.md @@ -0,0 +1,15 @@ +--- +description: Save the current browsing session state to a file +--- + +Save the current session state to: $ARGUMENTS + +Use the `session_save` tool to save: +- Current URL and page title +- All cookies +- localStorage data +- sessionStorage data + +If no path is specified, save to `session.json` in the current directory. + +This allows you to restore the session later with `/claude-browse:restore`. diff --git a/commands/scrape.md b/commands/scrape.md new file mode 100644 index 0000000..c40f3f3 --- /dev/null +++ b/commands/scrape.md @@ -0,0 +1,14 @@ +--- +description: Scrape content from a webpage +--- + +Scrape and extract content from: $ARGUMENTS + +Steps: +1. Navigate to the URL using `goto` +2. Wait for the page to load +3. Query the page structure to understand the layout +4. Extract the relevant content using `query` and `eval` tools +5. Return the structured data + +Focus on extracting meaningful content (text, links, data) rather than raw HTML. diff --git a/commands/screenshot.md b/commands/screenshot.md new file mode 100644 index 0000000..2829953 --- /dev/null +++ b/commands/screenshot.md @@ -0,0 +1,9 @@ +--- +description: Take a screenshot of the current page +--- + +Take a screenshot of the current browser page and analyze its contents. + +If the browser hasn't been started yet, ask for a URL first, navigate there, then take the screenshot. + +Use the `screenshot` tool with fullPage=$ARGUMENTS if specified (true/false), otherwise use default viewport. diff --git a/commands/start.md b/commands/start.md new file mode 100644 index 0000000..1bfb712 --- /dev/null +++ b/commands/start.md @@ -0,0 +1,11 @@ +--- +description: Start an interactive browsing session +--- + +Start an interactive browser session. Use the browser tools to: +1. Navigate to URLs with `goto` +2. Take screenshots to see page content +3. Query elements with CSS selectors +4. Click and type to interact with pages + +The browser is headless WebKit (Safari engine). Start by asking what URL to visit. diff --git a/package.json b/package.json index e7ee5ee..29b4c19 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aladac/claude-browse", - "version": "0.1.2", + "version": "0.2.0", "description": "Headless browser automation for Claude Code using Playwright WebKit", "type": "module", "main": "dist/index.js", diff --git a/src/browser.ts b/src/browser.ts index 97341a7..9a468ca 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -44,6 +44,16 @@ export class ClaudeBrowser { return this.page; } + /** Get the current page instance (for advanced usage) */ + getPage(): Page | null { + return this.page; + } + + /** Get the browser context (for advanced usage like cookies) */ + getContext(): BrowserContext | null { + return this.context; + } + async goto(url: string): Promise<{ url: string; title: string }> { const page = this.ensurePage(); await page.goto(url, { waitUntil: 'networkidle' }); diff --git a/src/mcp.ts b/src/mcp.ts index 1fcfe27..6dc2a2a 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; import { ClaudeBrowser } from './browser.js'; @@ -8,6 +8,7 @@ import { type CommandLike, type ResultLike, stderrLogger as log } from './logger const browser = new ClaudeBrowser({ headless: true, width: 1280, height: 800 }); let launched = false; +let currentScreenshotBuffer: Buffer | null = null; async function ensureLaunched(): Promise { if (!launched) { @@ -188,6 +189,124 @@ server.tool( }) ); +// Session management +server.tool( + 'close', + 'Close the browser and end the current session', + {}, + withLogging('close', async () => { + if (launched) { + await browser.close(); + launched = false; + } + return textResult(JSON.stringify({ ok: true, message: 'Browser closed' })); + }) +); + +server.tool( + 'session_save', + 'Save the current session state (URL, cookies, localStorage, sessionStorage) to a JSON file', + { + path: z.string().optional().default('session.json').describe('Path to save session file'), + }, + withLogging('session_save', async ({ path }) => { + await ensureLaunched(); + const { writeFile } = await import('node:fs/promises'); + const { resolve } = await import('node:path'); + + const page = browser.getPage(); + const context = browser.getContext(); + if (!page || !context) { + return textResult(JSON.stringify({ ok: false, error: 'No active page' })); + } + + const url = page.url(); + const title = await page.title(); + const cookies = await context.cookies(); + + // Get localStorage and sessionStorage (runs in browser context) + const storage = await page.evaluate(`({ + localStorage: Object.fromEntries( + Array.from({ length: localStorage.length }, (_, i) => localStorage.key(i)) + .filter(k => k !== null) + .map(k => [k, localStorage.getItem(k) || '']) + ), + sessionStorage: Object.fromEntries( + Array.from({ length: sessionStorage.length }, (_, i) => sessionStorage.key(i)) + .filter(k => k !== null) + .map(k => [k, sessionStorage.getItem(k) || '']) + ), + })`) as { localStorage: Record; sessionStorage: Record }; + + const sessionData = { + url, + title, + cookies, + localStorage: storage.localStorage, + sessionStorage: storage.sessionStorage, + savedAt: new Date().toISOString(), + }; + + const resolvedPath = resolve(path); + await writeFile(resolvedPath, JSON.stringify(sessionData, null, 2)); + return textResult( + JSON.stringify({ ok: true, path: resolvedPath, url, cookieCount: cookies.length }) + ); + }) +); + +server.tool( + 'session_restore', + 'Restore a previously saved session state from a JSON file', + { + path: z.string().optional().default('session.json').describe('Path to session file'), + }, + withLogging('session_restore', async ({ path }) => { + await ensureLaunched(); + const { readFile } = await import('node:fs/promises'); + const { resolve } = await import('node:path'); + + const resolvedPath = resolve(path); + const data = JSON.parse(await readFile(resolvedPath, 'utf-8')); + + const page = browser.getPage(); + const context = browser.getContext(); + if (!page || !context) { + return textResult(JSON.stringify({ ok: false, error: 'No active page' })); + } + + // Restore cookies first + if (data.cookies?.length > 0) { + await context.addCookies(data.cookies); + } + + // Navigate to saved URL + if (data.url) { + await page.goto(data.url, { waitUntil: 'networkidle' }); + } + + // Restore storage (runs in browser context) + const local = data.localStorage || {}; + const session = data.sessionStorage || {}; + await page.evaluate( + `((data) => { + for (const [k, v] of Object.entries(data.local)) localStorage.setItem(k, v); + for (const [k, v] of Object.entries(data.session)) sessionStorage.setItem(k, v); + })(${JSON.stringify({ local, session })})` + ); + + return textResult( + JSON.stringify({ + ok: true, + url: data.url, + title: data.title, + cookiesRestored: data.cookies?.length || 0, + savedAt: data.savedAt, + }) + ); + }) +); + // Image processing server.tool( 'favicon', @@ -290,7 +409,327 @@ server.tool( }) ); +// ============================================================================ +// MCP Resources - Browser state accessible via @ mentions +// ============================================================================ + +// Resource: browser://state - Current browser state (URL, title, launched status) +server.resource( + 'Browser State', + 'browser://state', + { + description: 'Current browser state including URL, title, and status', + mimeType: 'application/json', + }, + async () => { + if (!launched) { + return { + contents: [ + { + uri: 'browser://state', + mimeType: 'application/json', + text: JSON.stringify({ launched: false, url: null, title: null }), + }, + ], + }; + } + const state = await browser.getUrl(); + return { + contents: [ + { + uri: 'browser://state', + mimeType: 'application/json', + text: JSON.stringify({ launched: true, ...state }), + }, + ], + }; + } +); + +// Resource: browser://html - Current page HTML content +server.resource( + 'Page HTML', + 'browser://html', + { description: 'HTML content of the current page (truncated to 10KB)', mimeType: 'text/html' }, + async () => { + if (!launched) { + return { + contents: [ + { + uri: 'browser://html', + mimeType: 'text/plain', + text: 'Browser not launched. Use goto tool first.', + }, + ], + }; + } + const html = await browser.getHtml(false); + return { + contents: [ + { + uri: 'browser://html', + mimeType: 'text/html', + text: html, + }, + ], + }; + } +); + +// Resource: browser://html/full - Full page HTML content +server.resource( + 'Full Page HTML', + 'browser://html/full', + { description: 'Complete HTML content of the current page', mimeType: 'text/html' }, + async () => { + if (!launched) { + return { + contents: [ + { + uri: 'browser://html/full', + mimeType: 'text/plain', + text: 'Browser not launched. Use goto tool first.', + }, + ], + }; + } + const html = await browser.getHtml(true); + return { + contents: [ + { + uri: 'browser://html/full', + mimeType: 'text/html', + text: html, + }, + ], + }; + } +); + +// Resource: browser://screenshot - Current page screenshot (base64) +server.resource( + 'Page Screenshot', + 'browser://screenshot', + { description: 'Screenshot of the current page as base64 PNG', mimeType: 'image/png' }, + async () => { + if (!launched) { + return { + contents: [ + { + uri: 'browser://screenshot', + mimeType: 'text/plain', + text: 'Browser not launched. Use goto tool first.', + }, + ], + }; + } + const result = await browser.screenshot(undefined, false); + currentScreenshotBuffer = result.buffer || null; + return { + contents: [ + { + uri: 'browser://screenshot', + mimeType: 'image/png', + blob: result.buffer?.toString('base64') || '', + }, + ], + }; + } +); + +// ============================================================================ +// MCP Prompts - Common workflows accessible via / commands +// ============================================================================ + +// Prompt: Analyze current page +server.prompt('analyze_page', 'Analyze the current page content and structure', async () => { + if (!launched) { + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: 'The browser is not launched yet. Please use the goto tool to navigate to a URL first, then I can analyze the page.', + }, + }, + ], + }; + } + const state = await browser.getUrl(); + const html = await browser.getHtml(false); + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `Analyze the following webpage: + +URL: ${state.url} +Title: ${state.title} + +HTML Content (truncated): +\`\`\`html +${html} +\`\`\` + +Please provide: +1. A summary of the page purpose and content +2. Key interactive elements (forms, buttons, links) +3. Any notable structure or patterns +4. Suggestions for what actions might be useful`, + }, + }, + ], + }; +}); + +// Prompt: Extract data from page +server.prompt( + 'extract_data', + 'Extract structured data from the current page', + { selector: z.string().optional().describe('CSS selector to focus extraction (optional)') }, + async ({ selector }) => { + if (!launched) { + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: 'The browser is not launched yet. Please use the goto tool to navigate to a URL first.', + }, + }, + ], + }; + } + const state = await browser.getUrl(); + let elements: { tag: string; text: string; attributes: Record }[] = []; + if (selector) { + elements = await browser.query(selector); + } + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `Extract structured data from this webpage: + +URL: ${state.url} +Title: ${state.title} +${selector ? `\nSelector: ${selector}\nMatched Elements: ${elements.length}\n\nElements:\n${JSON.stringify(elements, null, 2)}` : ''} + +Please: +1. Use the query tool to find relevant data elements +2. Extract and structure the data in a useful format (JSON, table, etc.) +3. Identify patterns that could help with similar pages`, + }, + }, + ], + }; + } +); + +// Prompt: Navigate and interact +server.prompt( + 'navigate_to', + 'Navigate to a URL and describe what you find', + { url: z.string().url().describe('URL to navigate to') }, + async ({ url }) => { + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `Please navigate to ${url} and: + +1. Use the goto tool to navigate there +2. Take a screenshot to see the page +3. Describe what you see +4. Identify the main interactive elements +5. Suggest what actions might be useful + +Start by navigating to the URL.`, + }, + }, + ], + }; + } +); + +// Prompt: Fill form +server.prompt( + 'fill_form', + 'Help fill out a form on the current page', + { formData: z.string().optional().describe('JSON object with field names and values to fill') }, + async ({ formData }) => { + if (!launched) { + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: 'The browser is not launched yet. Please use the goto tool to navigate to a page with a form first.', + }, + }, + ], + }; + } + const state = await browser.getUrl(); + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `Help fill out a form on this page: + +URL: ${state.url} +Title: ${state.title} +${formData ? `\nData to fill: ${formData}` : ''} + +Please: +1. Use the query tool to find form inputs (input, textarea, select) +2. Identify required fields and their types +3. Use the type tool to fill in each field +4. Report what was filled and any issues encountered`, + }, + }, + ], + }; + } +); + +// Prompt: Screenshot comparison +server.prompt('compare_screenshots', 'Take screenshots and compare changes', async () => { + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `I'll help you compare page states: + +1. Take an initial screenshot +2. Perform some action (click, navigate, etc.) +3. Take another screenshot +4. Describe the differences + +What action would you like me to perform between screenshots?`, + }, + }, + ], + }; +}); + +// ============================================================================ // Start server +// ============================================================================ + async function main(): Promise { const transport = new StdioServerTransport(); await server.connect(transport);