From 52c6c92ae88c1f56c753e79a650e44efa5c3de7d Mon Sep 17 00:00:00 2001 From: kjtae1 Date: Thu, 22 Jan 2026 10:01:47 +0900 Subject: [PATCH 1/5] feat: Add MCP server for browser automation - Add Model Context Protocol server with 50+ automation tools - Enable LLMs (Claude Desktop, Cursor) to control agent-browser - Direct BrowserManager integration for optimal performance - Support all core features: navigation, interactions, tabs, storage - Include comprehensive documentation and setup guides - Add basic test suite with vitest - Session-based multi-browser support --- mcp-server/.gitignore | 4 + mcp-server/CHANGELOG.md | 41 + mcp-server/README.md | 183 ++++ mcp-server/SETUP.md | 251 +++++ mcp-server/example-config-claude.json | 11 + mcp-server/example-config-cursor.json | 13 + mcp-server/package.json | 35 + mcp-server/src/index.ts | 1327 +++++++++++++++++++++++++ mcp-server/test/basic.test.ts | 18 + mcp-server/tsconfig.json | 17 + mcp-server/vitest.config.ts | 8 + 11 files changed, 1908 insertions(+) create mode 100644 mcp-server/.gitignore create mode 100644 mcp-server/CHANGELOG.md create mode 100644 mcp-server/README.md create mode 100644 mcp-server/SETUP.md create mode 100644 mcp-server/example-config-claude.json create mode 100644 mcp-server/example-config-cursor.json create mode 100644 mcp-server/package.json create mode 100644 mcp-server/src/index.ts create mode 100644 mcp-server/test/basic.test.ts create mode 100644 mcp-server/tsconfig.json create mode 100644 mcp-server/vitest.config.ts diff --git a/mcp-server/.gitignore b/mcp-server/.gitignore new file mode 100644 index 00000000..dd6e803c --- /dev/null +++ b/mcp-server/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +*.log +.DS_Store diff --git a/mcp-server/CHANGELOG.md b/mcp-server/CHANGELOG.md new file mode 100644 index 00000000..6d90ab8c --- /dev/null +++ b/mcp-server/CHANGELOG.md @@ -0,0 +1,41 @@ +# Changelog + +All notable changes to agent-browser-mcp-server will be documented in this file. + +## [2.0.0] - 2026-01-22 + +### Added +- 🎉 **50+ browser automation tools** for LLMs +- Direct `BrowserManager` integration (no subprocess overhead) +- Full navigation support (navigate, back, forward, reload) +- Complete interaction tools (click, fill, type, hover, select, etc.) +- Information gathering (snapshot, get text, screenshots, etc.) +- Multi-tab support (new, switch, close, list) +- Cookie management (get, set, clear) +- Storage management (localStorage, sessionStorage) +- Frame handling (switch to iframe, back to main) +- Dialog handling (accept, dismiss) +- Network request tracking +- Viewport and geolocation settings +- Console and error monitoring +- Session management for parallel browsers + +### Changed +- Architecture: Direct import instead of CLI subprocess +- Headed mode by default (browser window visible) +- Improved error handling and reporting +- Better TypeScript types + +### Documentation +- Complete README with 50+ tools documented +- SETUP.md with step-by-step installation +- Example config files for Cursor and Claude Desktop +- Troubleshooting guide + +## [1.0.0] - Initial Release + +### Added +- Basic MCP server implementation +- CLI subprocess approach +- Core browser commands +- README documentation diff --git a/mcp-server/README.md b/mcp-server/README.md new file mode 100644 index 00000000..f48893d5 --- /dev/null +++ b/mcp-server/README.md @@ -0,0 +1,183 @@ +# agent-browser MCP Server + +[Model Context Protocol](https://modelcontextprotocol.io) server for [agent-browser](https://github.com/vercel-labs/agent-browser). Enables LLMs to control browsers through 50+ automation tools. + +## Installation + +### Prerequisites + +```bash +npm install -g agent-browser +agent-browser install +``` + +### Install MCP Server + +```bash +cd mcp-server +npm install +npm run build +``` + +## Configuration + +### Cursor + +Add to `~/.cursor/mcp.json`: + +```json +{ + "mcpServers": { + "agent-browser": { + "command": "node", + "args": ["/path/to/agent-browser/mcp-server/dist/index.js"] + } + } +} +``` + +### Claude Desktop + +**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` + +**Windows**: `%APPDATA%\Claude\claude_desktop_config.json` + +```json +{ + "mcpServers": { + "agent-browser": { + "command": "node", + "args": ["/path/to/agent-browser/mcp-server/dist/index.js"] + } + } +} +``` + +## Usage + +Once configured, LLMs can use browser automation: + +``` +"Open https://example.com and take a screenshot" +"Navigate to GitHub and click the login button" +"Fill the email field with test@example.com" +``` + +## Tools + +The server provides 50+ browser automation tools: + +### Navigation +- `browser_navigate`, `browser_back`, `browser_forward`, `browser_reload` + +### Interactions +- `browser_click`, `browser_fill`, `browser_type`, `browser_press` +- `browser_hover`, `browser_select`, `browser_check`, `browser_scroll` +- `browser_drag`, `browser_upload` + +### Information +- `browser_snapshot` - Get accessibility tree with refs (AI-optimized) +- `browser_get_text`, `browser_get_html`, `browser_get_value` +- `browser_get_title`, `browser_get_url`, `browser_screenshot` + +### Tabs +- `browser_tab_new`, `browser_tab_switch`, `browser_tab_close`, `browser_tab_list` + +### Storage +- `browser_cookies_get`, `browser_cookies_set`, `browser_cookies_clear` +- `browser_storage_get`, `browser_storage_set`, `browser_storage_clear` + +### Advanced +- `browser_evaluate` - Execute JavaScript +- `browser_frame_switch` - Work with iframes +- `browser_dialog_accept` - Handle alerts/confirms +- `browser_network_requests` - Track network activity + +[View complete tool list](SETUP.md#available-tools-50) + +## Workflow Pattern + +```javascript +// 1. Navigate +browser_navigate({ url: "https://example.com" }) + +// 2. Get page structure +browser_snapshot({ interactive: true, compact: true }) + +// 3. Interact using refs +browser_click({ selector: "@e5" }) // Use ref from snapshot +browser_fill({ selector: "@e3", value: "text" }) + +// 4. Extract data +browser_get_text({ selector: "@e1" }) +browser_screenshot({ fullPage: false }) +``` + +## Architecture + +``` +LLM (Claude/Cursor) + ↓ MCP Protocol +MCP Server (this package) + ↓ Direct import +BrowserManager (agent-browser) + ↓ +Playwright +``` + +This server directly imports `BrowserManager` from agent-browser for optimal performance. + +## Development + +```bash +npm run dev # Watch mode +npm run build # Production build +npm run format # Format code with Prettier +npm test # Run tests +``` + +## Configuration Options + +### Headless Mode + +Edit `src/index.ts` line ~680: + +```typescript +headless: false, // Change to true for headless mode +``` + +### Session Management + +Multiple browser instances: + +```javascript +browser_navigate({ url: "site1.com", session: "task1" }) +browser_navigate({ url: "site2.com", session: "task2" }) +``` + +## Troubleshooting + +**agent-browser not found** +```bash +npm install -g agent-browser +agent-browser install +``` + +**Module not found** +- Ensure agent-browser is installed globally +- Check the path in MCP config is correct + +**Linux dependencies** +```bash +agent-browser install --with-deps +``` + +See [SETUP.md](SETUP.md) for detailed troubleshooting. + +## License + +Apache-2.0 + +## Credits + +Built on [agent-browser](https://github.com/vercel-labs/agent-browser) by Vercel Labs. diff --git a/mcp-server/SETUP.md b/mcp-server/SETUP.md new file mode 100644 index 00000000..6751c496 --- /dev/null +++ b/mcp-server/SETUP.md @@ -0,0 +1,251 @@ +# Setup Guide + +Complete installation guide for agent-browser MCP Server. + +## Step 1: Install Prerequisites + +### Install Node.js +- Download from [nodejs.org](https://nodejs.org/) (v20 or later recommended) +- Verify: `node --version` + +### Install agent-browser +```bash +npm install -g agent-browser +agent-browser install +``` + +Verify installation: +```bash +agent-browser --version +``` + +## Step 2: Install MCP Server + +### Option A: From Source (Development) + +1. Clone the repository: +```bash +git clone https://github.com/YOUR_USERNAME/agent-browser-mcp-server.git +cd agent-browser-mcp-server +``` + +2. Install dependencies: +```bash +npm install +``` + +3. Build: +```bash +npm run build +``` + +4. Note the absolute path to `dist/index.js`: +```bash +pwd # On Unix/Mac +cd # On Windows +``` + +### Option B: Install Globally (Recommended) + +```bash +npm install -g agent-browser-mcp-server +``` + +Then you can use the `agent-browser-mcp` command directly. + +## Step 3: Configure Your MCP Client + +### For Cursor + +1. Open Cursor settings directory: + - **macOS/Linux**: `~/.cursor/` + - **Windows**: `%USERPROFILE%\.cursor\` + +2. Create or edit `mcp.json`: + +**If installed from source:** +```json +{ + "mcpServers": { + "agent-browser": { + "command": "node", + "args": ["/absolute/path/to/agent-browser-mcp-server/dist/index.js"] + } + } +} +``` + +**If installed globally:** +```json +{ + "mcpServers": { + "agent-browser": { + "command": "agent-browser-mcp" + } + } +} +``` + +3. Restart Cursor + +### For Claude Desktop + +1. Locate config file: + - **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` + - **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` + - **Linux**: `~/.config/Claude/claude_desktop_config.json` + +2. Edit config: + +**If installed from source:** +```json +{ + "mcpServers": { + "agent-browser": { + "command": "node", + "args": ["/absolute/path/to/agent-browser-mcp-server/dist/index.js"] + } + } +} +``` + +**Example paths:** +- macOS: `"/Users/username/projects/agent-browser-mcp-server/dist/index.js"` +- Windows: `"C:\\Users\\username\\projects\\agent-browser-mcp-server\\dist\\index.js"` +- Linux: `"/home/username/projects/agent-browser-mcp-server/dist/index.js"` + +3. Restart Claude Desktop + +## Step 4: Test It! + +After restarting your MCP client, try asking: + +``` +"Open https://example.com and take a screenshot" +``` + +The AI should: +1. Launch a browser +2. Navigate to the site +3. Capture and show you a screenshot + +## Troubleshooting + +### "agent-browser command not found" + +**Solution:** +```bash +npm install -g agent-browser +agent-browser install +``` + +Verify it's in PATH: +```bash +which agent-browser # Unix/Mac +where agent-browser # Windows +``` + +### "Cannot find module '../../dist/browser.js'" + +**Cause:** MCP server can't find agent-browser installation. + +**Solution:** +1. Make sure agent-browser is installed globally: `npm list -g agent-browser` +2. Or install it in the same directory: `npm install agent-browser` + +### "MCP server not starting" + +**Check:** +1. Is Node.js installed? `node --version` +2. Is the path in config correct? (Use absolute paths!) +3. Was the server built? Check if `dist/index.js` exists +4. Check client logs: + - Cursor: Open Developer Tools (Help → Toggle Developer Tools) + - Claude Desktop: Check console logs + +### Browser doesn't appear (headless mode) + +By default, browser runs in **headed mode** (visible window). + +To change to headless: +1. Edit `src/index.ts` line 232 +2. Change `headless: false` to `headless: true` +3. Rebuild: `npm run build` +4. Restart MCP client + +### Windows-specific issues + +**Node.js not recognized:** +- Make sure Node.js is in PATH +- Restart terminal/PowerShell after installing Node.js + +**Path escaping:** +- Use double backslashes in JSON: `"C:\\path\\to\\file"` +- Or use forward slashes: `"C:/path/to/file"` + +**Permission errors:** +- Run terminal as Administrator if needed +- Check antivirus isn't blocking browser spawn + +## Platform-Specific Notes + +### macOS +- First time running may trigger security prompt +- Go to System Preferences → Security & Privacy → Allow + +### Linux +- Install browser dependencies: + ```bash + agent-browser install --with-deps + ``` + Or manually: + ```bash + npx playwright install-deps chromium + ``` + +### Windows +- Use PowerShell or Command Prompt (not Git Bash for config) +- Check Windows Defender isn't blocking + +## Advanced Configuration + +### Change Browser Type + +Edit `src/index.ts`: +```typescript +await browser.launch({ + browser: 'chromium', // or 'firefox', 'webkit' + headless: false, +}); +``` + +### Custom Viewport Size + +In `src/index.ts`: +```typescript +await browser.launch({ + viewport: { width: 1920, height: 1080 }, +}); +``` + +### Multiple Sessions + +The AI can use different sessions automatically: +``` +"Open Google in session 'task1' and GitHub in session 'task2'" +``` + +Each session is an isolated browser instance. + +## Getting Help + +- Check [GitHub Issues](https://github.com/YOUR_USERNAME/agent-browser-mcp-server/issues) +- agent-browser docs: [github.com/vercel-labs/agent-browser](https://github.com/vercel-labs/agent-browser) +- MCP Protocol docs: [modelcontextprotocol.io](https://modelcontextprotocol.io/) + +## Next Steps + +Once working: +1. Try the [Usage Examples](README.md#usage-examples) +2. Explore all [50+ tools](README.md#available-tools-50) +3. Build your own automation workflows! diff --git a/mcp-server/example-config-claude.json b/mcp-server/example-config-claude.json new file mode 100644 index 00000000..5e1e484c --- /dev/null +++ b/mcp-server/example-config-claude.json @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "agent-browser": { + "comment": "macOS: Edit ~/Library/Application Support/Claude/claude_desktop_config.json", + "comment2": "Windows: Edit %APPDATA%\\Claude\\claude_desktop_config.json", + "comment3": "Replace path below with your actual installation path", + "command": "node", + "args": ["/absolute/path/to/agent-browser/mcp-server/dist/index.js"] + } + } +} diff --git a/mcp-server/example-config-cursor.json b/mcp-server/example-config-cursor.json new file mode 100644 index 00000000..72200643 --- /dev/null +++ b/mcp-server/example-config-cursor.json @@ -0,0 +1,13 @@ +{ + "mcpServers": { + "agent-browser": { + "comment": "Option 1: Using absolute path (replace with your actual path)", + "command": "node", + "args": ["/absolute/path/to/agent-browser/mcp-server/dist/index.js"] + }, + "agent-browser-global": { + "comment": "Option 2: If installed globally with 'npm install -g agent-browser-mcp-server'", + "command": "agent-browser-mcp" + } + } +} diff --git a/mcp-server/package.json b/mcp-server/package.json new file mode 100644 index 00000000..524d2ba9 --- /dev/null +++ b/mcp-server/package.json @@ -0,0 +1,35 @@ +{ + "name": "agent-browser-mcp-server", + "version": "2.0.0", + "description": "MCP server for agent-browser - Full browser automation for LLMs", + "type": "module", + "main": "dist/index.js", + "bin": { + "agent-browser-mcp": "./dist/index.js" + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "start": "node dist/index.js", + "prepare": "npm run build", + "format": "prettier --write 'src/**/*.ts'", + "format:check": "prettier --check 'src/**/*.ts'", + "test": "vitest run", + "test:watch": "vitest" + }, + "keywords": ["mcp", "agent-browser", "browser-automation", "llm", "ai", "playwright"], + "author": "", + "license": "Apache-2.0", + "peerDependencies": { + "agent-browser": ">=0.0.1" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^0.5.0" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "prettier": "^3.7.4", + "typescript": "^5.3.0", + "vitest": "^4.0.16" + } +} diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts new file mode 100644 index 00000000..ecad6f99 --- /dev/null +++ b/mcp-server/src/index.ts @@ -0,0 +1,1327 @@ +#!/usr/bin/env node + +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { + CallToolRequestSchema, + ListToolsRequestSchema, + Tool, +} from '@modelcontextprotocol/sdk/types.js'; +import { BrowserManager } from '../../dist/browser.js'; + +// Session-based browser managers +const browsers = new Map(); + +function getBrowser(session: string = 'default'): BrowserManager { + if (!browsers.has(session)) { + browsers.set(session, new BrowserManager()); + } + return browsers.get(session)!; +} + +/** + * MCP Tools for agent-browser - ALL FEATURES + */ +const tools: Tool[] = [ + // ============= NAVIGATION ============= + { + name: 'browser_navigate', + description: 'Navigate to a URL in the browser', + inputSchema: { + type: 'object', + properties: { + url: { type: 'string', description: 'URL to navigate to' }, + session: { type: 'string', default: 'default' }, + }, + required: ['url'], + }, + }, + { + name: 'browser_back', + description: 'Go back in browser history', + inputSchema: { + type: 'object', + properties: { + session: { type: 'string', default: 'default' }, + }, + }, + }, + { + name: 'browser_forward', + description: 'Go forward in browser history', + inputSchema: { + type: 'object', + properties: { + session: { type: 'string', default: 'default' }, + }, + }, + }, + { + name: 'browser_reload', + description: 'Reload the current page', + inputSchema: { + type: 'object', + properties: { + session: { type: 'string', default: 'default' }, + }, + }, + }, + + // ============= INTERACTIONS ============= + { + name: 'browser_click', + description: 'Click an element', + inputSchema: { + type: 'object', + properties: { + selector: { type: 'string', description: 'Selector (ref or CSS)' }, + session: { type: 'string', default: 'default' }, + }, + required: ['selector'], + }, + }, + { + name: 'browser_dblclick', + description: 'Double-click an element', + inputSchema: { + type: 'object', + properties: { + selector: { type: 'string' }, + session: { type: 'string', default: 'default' }, + }, + required: ['selector'], + }, + }, + { + name: 'browser_fill', + description: 'Clear and fill an input field', + inputSchema: { + type: 'object', + properties: { + selector: { type: 'string' }, + value: { type: 'string' }, + session: { type: 'string', default: 'default' }, + }, + required: ['selector', 'value'], + }, + }, + { + name: 'browser_type', + description: 'Type text into an element (without clearing)', + inputSchema: { + type: 'object', + properties: { + selector: { type: 'string' }, + text: { type: 'string' }, + session: { type: 'string', default: 'default' }, + }, + required: ['selector', 'text'], + }, + }, + { + name: 'browser_press', + description: 'Press a key (e.g., Enter, Tab, Control+a)', + inputSchema: { + type: 'object', + properties: { + key: { type: 'string', description: 'Key to press' }, + session: { type: 'string', default: 'default' }, + }, + required: ['key'], + }, + }, + { + name: 'browser_hover', + description: 'Hover over an element', + inputSchema: { + type: 'object', + properties: { + selector: { type: 'string' }, + session: { type: 'string', default: 'default' }, + }, + required: ['selector'], + }, + }, + { + name: 'browser_focus', + description: 'Focus an element', + inputSchema: { + type: 'object', + properties: { + selector: { type: 'string' }, + session: { type: 'string', default: 'default' }, + }, + required: ['selector'], + }, + }, + { + name: 'browser_select', + description: 'Select option in a dropdown', + inputSchema: { + type: 'object', + properties: { + selector: { type: 'string' }, + value: { type: 'string', description: 'Option value to select' }, + session: { type: 'string', default: 'default' }, + }, + required: ['selector', 'value'], + }, + }, + { + name: 'browser_check', + description: 'Check a checkbox', + inputSchema: { + type: 'object', + properties: { + selector: { type: 'string' }, + session: { type: 'string', default: 'default' }, + }, + required: ['selector'], + }, + }, + { + name: 'browser_uncheck', + description: 'Uncheck a checkbox', + inputSchema: { + type: 'object', + properties: { + selector: { type: 'string' }, + session: { type: 'string', default: 'default' }, + }, + required: ['selector'], + }, + }, + { + name: 'browser_scroll', + description: 'Scroll the page', + inputSchema: { + type: 'object', + properties: { + direction: { + type: 'string', + enum: ['up', 'down', 'left', 'right'], + description: 'Direction to scroll', + }, + pixels: { type: 'number', description: 'Pixels to scroll (default: 100)' }, + session: { type: 'string', default: 'default' }, + }, + required: ['direction'], + }, + }, + { + name: 'browser_scroll_into_view', + description: 'Scroll element into view', + inputSchema: { + type: 'object', + properties: { + selector: { type: 'string' }, + session: { type: 'string', default: 'default' }, + }, + required: ['selector'], + }, + }, + { + name: 'browser_drag', + description: 'Drag and drop from source to target', + inputSchema: { + type: 'object', + properties: { + source: { type: 'string', description: 'Source selector' }, + target: { type: 'string', description: 'Target selector' }, + session: { type: 'string', default: 'default' }, + }, + required: ['source', 'target'], + }, + }, + { + name: 'browser_upload', + description: 'Upload files to file input', + inputSchema: { + type: 'object', + properties: { + selector: { type: 'string' }, + files: { + type: 'array', + items: { type: 'string' }, + description: 'File paths to upload', + }, + session: { type: 'string', default: 'default' }, + }, + required: ['selector', 'files'], + }, + }, + + // ============= GET INFO ============= + { + name: 'browser_snapshot', + description: 'Get accessibility snapshot with refs', + inputSchema: { + type: 'object', + properties: { + interactive: { type: 'boolean', default: true }, + compact: { type: 'boolean', default: true }, + depth: { type: 'number' }, + session: { type: 'string', default: 'default' }, + }, + }, + }, + { + name: 'browser_get_text', + description: 'Get text content from element', + inputSchema: { + type: 'object', + properties: { + selector: { type: 'string' }, + session: { type: 'string', default: 'default' }, + }, + required: ['selector'], + }, + }, + { + name: 'browser_get_html', + description: 'Get innerHTML of element', + inputSchema: { + type: 'object', + properties: { + selector: { type: 'string' }, + session: { type: 'string', default: 'default' }, + }, + required: ['selector'], + }, + }, + { + name: 'browser_get_value', + description: 'Get value of input element', + inputSchema: { + type: 'object', + properties: { + selector: { type: 'string' }, + session: { type: 'string', default: 'default' }, + }, + required: ['selector'], + }, + }, + { + name: 'browser_get_attribute', + description: 'Get attribute value from element', + inputSchema: { + type: 'object', + properties: { + selector: { type: 'string' }, + attribute: { type: 'string', description: 'Attribute name' }, + session: { type: 'string', default: 'default' }, + }, + required: ['selector', 'attribute'], + }, + }, + { + name: 'browser_get_title', + description: 'Get page title', + inputSchema: { + type: 'object', + properties: { + session: { type: 'string', default: 'default' }, + }, + }, + }, + { + name: 'browser_get_url', + description: 'Get current URL', + inputSchema: { + type: 'object', + properties: { + session: { type: 'string', default: 'default' }, + }, + }, + }, + { + name: 'browser_get_count', + description: 'Count matching elements', + inputSchema: { + type: 'object', + properties: { + selector: { type: 'string' }, + session: { type: 'string', default: 'default' }, + }, + required: ['selector'], + }, + }, + { + name: 'browser_get_bounding_box', + description: 'Get bounding box of element', + inputSchema: { + type: 'object', + properties: { + selector: { type: 'string' }, + session: { type: 'string', default: 'default' }, + }, + required: ['selector'], + }, + }, + + // ============= CHECK STATE ============= + { + name: 'browser_is_visible', + description: 'Check if element is visible', + inputSchema: { + type: 'object', + properties: { + selector: { type: 'string' }, + session: { type: 'string', default: 'default' }, + }, + required: ['selector'], + }, + }, + { + name: 'browser_is_enabled', + description: 'Check if element is enabled', + inputSchema: { + type: 'object', + properties: { + selector: { type: 'string' }, + session: { type: 'string', default: 'default' }, + }, + required: ['selector'], + }, + }, + { + name: 'browser_is_checked', + description: 'Check if checkbox is checked', + inputSchema: { + type: 'object', + properties: { + selector: { type: 'string' }, + session: { type: 'string', default: 'default' }, + }, + required: ['selector'], + }, + }, + + // ============= WAIT ============= + { + name: 'browser_wait', + description: 'Wait for element, time, or condition', + inputSchema: { + type: 'object', + properties: { + selector: { type: 'string', description: 'Wait for element' }, + timeout: { type: 'number', description: 'Timeout in ms' }, + session: { type: 'string', default: 'default' }, + }, + }, + }, + + // ============= MOUSE ============= + { + name: 'browser_mouse_move', + description: 'Move mouse to coordinates', + inputSchema: { + type: 'object', + properties: { + x: { type: 'number' }, + y: { type: 'number' }, + session: { type: 'string', default: 'default' }, + }, + required: ['x', 'y'], + }, + }, + + // ============= SCREENSHOT & PDF ============= + { + name: 'browser_screenshot', + description: 'Take screenshot', + inputSchema: { + type: 'object', + properties: { + fullPage: { type: 'boolean', default: false }, + session: { type: 'string', default: 'default' }, + }, + }, + }, + { + name: 'browser_pdf', + description: 'Save page as PDF', + inputSchema: { + type: 'object', + properties: { + session: { type: 'string', default: 'default' }, + }, + }, + }, + + // ============= EVALUATE ============= + { + name: 'browser_evaluate', + description: 'Execute JavaScript', + inputSchema: { + type: 'object', + properties: { + script: { type: 'string' }, + session: { type: 'string', default: 'default' }, + }, + required: ['script'], + }, + }, + + // ============= TABS ============= + { + name: 'browser_tab_new', + description: 'Open new tab', + inputSchema: { + type: 'object', + properties: { + url: { type: 'string' }, + session: { type: 'string', default: 'default' }, + }, + }, + }, + { + name: 'browser_tab_switch', + description: 'Switch to tab by index', + inputSchema: { + type: 'object', + properties: { + index: { type: 'number' }, + session: { type: 'string', default: 'default' }, + }, + required: ['index'], + }, + }, + { + name: 'browser_tab_close', + description: 'Close tab', + inputSchema: { + type: 'object', + properties: { + index: { type: 'number' }, + session: { type: 'string', default: 'default' }, + }, + }, + }, + { + name: 'browser_tab_list', + description: 'List all tabs', + inputSchema: { + type: 'object', + properties: { + session: { type: 'string', default: 'default' }, + }, + }, + }, + + // ============= COOKIES ============= + { + name: 'browser_cookies_get', + description: 'Get all cookies', + inputSchema: { + type: 'object', + properties: { + session: { type: 'string', default: 'default' }, + }, + }, + }, + { + name: 'browser_cookies_set', + description: 'Set a cookie', + inputSchema: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + domain: { type: 'string' }, + path: { type: 'string' }, + session: { type: 'string', default: 'default' }, + }, + required: ['name', 'value'], + }, + }, + { + name: 'browser_cookies_clear', + description: 'Clear all cookies', + inputSchema: { + type: 'object', + properties: { + session: { type: 'string', default: 'default' }, + }, + }, + }, + + // ============= STORAGE ============= + { + name: 'browser_storage_get', + description: 'Get localStorage or sessionStorage', + inputSchema: { + type: 'object', + properties: { + type: { type: 'string', enum: ['local', 'session'] }, + key: { type: 'string', description: 'Optional key' }, + session: { type: 'string', default: 'default' }, + }, + required: ['type'], + }, + }, + { + name: 'browser_storage_set', + description: 'Set localStorage or sessionStorage value', + inputSchema: { + type: 'object', + properties: { + type: { type: 'string', enum: ['local', 'session'] }, + key: { type: 'string' }, + value: { type: 'string' }, + session: { type: 'string', default: 'default' }, + }, + required: ['type', 'key', 'value'], + }, + }, + { + name: 'browser_storage_clear', + description: 'Clear storage', + inputSchema: { + type: 'object', + properties: { + type: { type: 'string', enum: ['local', 'session'] }, + session: { type: 'string', default: 'default' }, + }, + required: ['type'], + }, + }, + + // ============= FRAMES ============= + { + name: 'browser_frame_switch', + description: 'Switch to iframe', + inputSchema: { + type: 'object', + properties: { + selector: { type: 'string', description: 'Frame selector' }, + session: { type: 'string', default: 'default' }, + }, + required: ['selector'], + }, + }, + { + name: 'browser_frame_main', + description: 'Switch back to main frame', + inputSchema: { + type: 'object', + properties: { + session: { type: 'string', default: 'default' }, + }, + }, + }, + + // ============= DIALOGS ============= + { + name: 'browser_dialog_accept', + description: 'Accept dialog (alert/confirm/prompt)', + inputSchema: { + type: 'object', + properties: { + text: { type: 'string', description: 'Text for prompt' }, + session: { type: 'string', default: 'default' }, + }, + }, + }, + { + name: 'browser_dialog_dismiss', + description: 'Dismiss dialog', + inputSchema: { + type: 'object', + properties: { + session: { type: 'string', default: 'default' }, + }, + }, + }, + + // ============= NETWORK ============= + { + name: 'browser_network_requests', + description: 'Get tracked network requests', + inputSchema: { + type: 'object', + properties: { + filter: { type: 'string', description: 'Filter by URL substring' }, + session: { type: 'string', default: 'default' }, + }, + }, + }, + + // ============= SETTINGS ============= + { + name: 'browser_set_viewport', + description: 'Set viewport size', + inputSchema: { + type: 'object', + properties: { + width: { type: 'number' }, + height: { type: 'number' }, + session: { type: 'string', default: 'default' }, + }, + required: ['width', 'height'], + }, + }, + { + name: 'browser_set_geolocation', + description: 'Set geolocation', + inputSchema: { + type: 'object', + properties: { + latitude: { type: 'number' }, + longitude: { type: 'number' }, + accuracy: { type: 'number', default: 0 }, + session: { type: 'string', default: 'default' }, + }, + required: ['latitude', 'longitude'], + }, + }, + + // ============= DEBUG ============= + { + name: 'browser_console', + description: 'Get console messages', + inputSchema: { + type: 'object', + properties: { + session: { type: 'string', default: 'default' }, + }, + }, + }, + { + name: 'browser_errors', + description: 'Get page errors', + inputSchema: { + type: 'object', + properties: { + session: { type: 'string', default: 'default' }, + }, + }, + }, + + // ============= SESSION ============= + { + name: 'browser_close', + description: 'Close browser', + inputSchema: { + type: 'object', + properties: { + session: { type: 'string', default: 'default' }, + }, + }, + }, + { + name: 'browser_session_list', + description: 'List active sessions', + inputSchema: { + type: 'object', + properties: {}, + }, + }, +]; + +/** + * Handle tool execution + */ +async function handleToolCall(name: string, args: any): Promise { + const session = args.session || 'default'; + const browser = getBrowser(session); + + try { + // Auto-launch browser if needed + if (!browser.isLaunched() && !name.includes('session_list') && !name.includes('close')) { + await browser.launch({ + id: 'auto', + action: 'launch', + headless: false, + }); + } + + switch (name) { + // ============= NAVIGATION ============= + case 'browser_navigate': { + const page = browser.getPage(); + await page.goto(args.url, { waitUntil: 'load' }); + const title = await page.title(); + return { + content: [{ type: 'text', text: `Navigated to ${args.url}\nTitle: ${title}` }], + }; + } + + case 'browser_back': { + const page = browser.getPage(); + await page.goBack(); + return { content: [{ type: 'text', text: 'Navigated back' }] }; + } + + case 'browser_forward': { + const page = browser.getPage(); + await page.goForward(); + return { content: [{ type: 'text', text: 'Navigated forward' }] }; + } + + case 'browser_reload': { + const page = browser.getPage(); + await page.reload(); + return { content: [{ type: 'text', text: 'Page reloaded' }] }; + } + + // ============= INTERACTIONS ============= + case 'browser_click': { + const locator = browser.getLocator(args.selector); + await locator.click(); + return { content: [{ type: 'text', text: `Clicked: ${args.selector}` }] }; + } + + case 'browser_dblclick': { + const locator = browser.getLocator(args.selector); + await locator.dblclick(); + return { content: [{ type: 'text', text: `Double-clicked: ${args.selector}` }] }; + } + + case 'browser_fill': { + const locator = browser.getLocator(args.selector); + await locator.fill(args.value); + return { + content: [{ type: 'text', text: `Filled ${args.selector} with: ${args.value}` }], + }; + } + + case 'browser_type': { + const locator = browser.getLocator(args.selector); + await locator.pressSequentially(args.text); + return { content: [{ type: 'text', text: `Typed into ${args.selector}` }] }; + } + + case 'browser_press': { + const page = browser.getPage(); + await page.keyboard.press(args.key); + return { content: [{ type: 'text', text: `Pressed: ${args.key}` }] }; + } + + case 'browser_hover': { + const locator = browser.getLocator(args.selector); + await locator.hover(); + return { content: [{ type: 'text', text: `Hovered: ${args.selector}` }] }; + } + + case 'browser_focus': { + const locator = browser.getLocator(args.selector); + await locator.focus(); + return { content: [{ type: 'text', text: `Focused: ${args.selector}` }] }; + } + + case 'browser_select': { + const locator = browser.getLocator(args.selector); + await locator.selectOption(args.value); + return { + content: [{ type: 'text', text: `Selected ${args.value} in ${args.selector}` }], + }; + } + + case 'browser_check': { + const locator = browser.getLocator(args.selector); + await locator.check(); + return { content: [{ type: 'text', text: `Checked: ${args.selector}` }] }; + } + + case 'browser_uncheck': { + const locator = browser.getLocator(args.selector); + await locator.uncheck(); + return { content: [{ type: 'text', text: `Unchecked: ${args.selector}` }] }; + } + + case 'browser_scroll': { + const page = browser.getPage(); + const pixels = args.pixels || 100; + const scroll = { + up: `window.scrollBy(0, -${pixels})`, + down: `window.scrollBy(0, ${pixels})`, + left: `window.scrollBy(-${pixels}, 0)`, + right: `window.scrollBy(${pixels}, 0)`, + }; + await page.evaluate(scroll[args.direction as keyof typeof scroll]); + return { + content: [{ type: 'text', text: `Scrolled ${args.direction} ${pixels}px` }], + }; + } + + case 'browser_scroll_into_view': { + const locator = browser.getLocator(args.selector); + await locator.scrollIntoViewIfNeeded(); + return { + content: [{ type: 'text', text: `Scrolled ${args.selector} into view` }], + }; + } + + case 'browser_drag': { + const source = browser.getLocator(args.source); + const target = browser.getLocator(args.target); + await source.dragTo(target); + return { + content: [{ type: 'text', text: `Dragged ${args.source} to ${args.target}` }], + }; + } + + case 'browser_upload': { + const locator = browser.getLocator(args.selector); + await locator.setInputFiles(args.files); + return { + content: [ + { + type: 'text', + text: `Uploaded ${args.files.length} file(s) to ${args.selector}`, + }, + ], + }; + } + + // ============= GET INFO ============= + case 'browser_snapshot': { + const snapshot = await browser.getSnapshot({ + interactive: args.interactive !== false, + compact: args.compact !== false, + maxDepth: args.depth, + }); + return { + content: [ + { + type: 'text', + text: `Page snapshot:\n\n${snapshot.tree}\n\nUse refs like @e1, @e2 to interact.`, + }, + ], + }; + } + + case 'browser_get_text': { + const locator = browser.getLocator(args.selector); + const text = await locator.textContent(); + return { + content: [{ type: 'text', text: `Text: ${text || '(empty)'}` }], + }; + } + + case 'browser_get_html': { + const locator = browser.getLocator(args.selector); + const html = await locator.innerHTML(); + return { content: [{ type: 'text', text: `HTML:\n${html}` }] }; + } + + case 'browser_get_value': { + const locator = browser.getLocator(args.selector); + const value = await locator.inputValue(); + return { content: [{ type: 'text', text: `Value: ${value}` }] }; + } + + case 'browser_get_attribute': { + const locator = browser.getLocator(args.selector); + const attr = await locator.getAttribute(args.attribute); + return { + content: [{ type: 'text', text: `${args.attribute}: ${attr || '(null)'}` }], + }; + } + + case 'browser_get_title': { + const page = browser.getPage(); + const title = await page.title(); + return { content: [{ type: 'text', text: `Title: ${title}` }] }; + } + + case 'browser_get_url': { + const page = browser.getPage(); + const url = page.url(); + return { content: [{ type: 'text', text: `URL: ${url}` }] }; + } + + case 'browser_get_count': { + const locator = browser.getLocator(args.selector); + const count = await locator.count(); + return { + content: [{ type: 'text', text: `Count: ${count} element(s)` }], + }; + } + + case 'browser_get_bounding_box': { + const locator = browser.getLocator(args.selector); + const box = await locator.boundingBox(); + return { + content: [ + { + type: 'text', + text: box + ? `Box: x=${box.x}, y=${box.y}, width=${box.width}, height=${box.height}` + : 'Element not visible', + }, + ], + }; + } + + // ============= CHECK STATE ============= + case 'browser_is_visible': { + const locator = browser.getLocator(args.selector); + const visible = await locator.isVisible(); + return { content: [{ type: 'text', text: `Visible: ${visible}` }] }; + } + + case 'browser_is_enabled': { + const locator = browser.getLocator(args.selector); + const enabled = await locator.isEnabled(); + return { content: [{ type: 'text', text: `Enabled: ${enabled}` }] }; + } + + case 'browser_is_checked': { + const locator = browser.getLocator(args.selector); + const checked = await locator.isChecked(); + return { content: [{ type: 'text', text: `Checked: ${checked}` }] }; + } + + // ============= WAIT ============= + case 'browser_wait': { + if (args.selector) { + const locator = browser.getLocator(args.selector); + await locator.waitFor({ + state: 'visible', + timeout: args.timeout, + }); + return { + content: [{ type: 'text', text: `Waited for ${args.selector} to be visible` }], + }; + } else if (args.timeout) { + await new Promise((resolve) => setTimeout(resolve, args.timeout)); + return { + content: [{ type: 'text', text: `Waited ${args.timeout}ms` }], + }; + } + throw new Error('Either selector or timeout required'); + } + + // ============= MOUSE ============= + case 'browser_mouse_move': { + const page = browser.getPage(); + await page.mouse.move(args.x, args.y); + return { + content: [{ type: 'text', text: `Moved mouse to (${args.x}, ${args.y})` }], + }; + } + + // ============= SCREENSHOT & PDF ============= + case 'browser_screenshot': { + const page = browser.getPage(); + const buffer = await page.screenshot({ + fullPage: args.fullPage || false, + type: 'png', + }); + return { + content: [ + { + type: 'image', + data: buffer.toString('base64'), + mimeType: 'image/png', + }, + ], + }; + } + + case 'browser_pdf': { + const page = browser.getPage(); + const buffer = await page.pdf(); + return { + content: [ + { + type: 'resource', + resource: { + uri: `data:application/pdf;base64,${buffer.toString('base64')}`, + mimeType: 'application/pdf', + }, + }, + ], + }; + } + + // ============= EVALUATE ============= + case 'browser_evaluate': { + const page = browser.getPage(); + const result = await page.evaluate(args.script); + return { + content: [{ type: 'text', text: `Result:\n${JSON.stringify(result, null, 2)}` }], + }; + } + + // ============= TABS ============= + case 'browser_tab_new': { + const result = await browser.newTab(); + if (args.url) { + const page = browser.getPage(); + await page.goto(args.url, { waitUntil: 'load' }); + } + return { + content: [ + { + type: 'text', + text: `New tab ${result.index} (total: ${result.total})${args.url ? `\nNavigated to: ${args.url}` : ''}`, + }, + ], + }; + } + + case 'browser_tab_switch': { + const result = await browser.switchTo(args.index); + return { + content: [{ type: 'text', text: `Switched to tab ${result.index}\nURL: ${result.url}` }], + }; + } + + case 'browser_tab_close': { + const result = await browser.closeTab(args.index); + return { + content: [ + { + type: 'text', + text: `Closed tab ${result.closed}\nRemaining: ${result.remaining}`, + }, + ], + }; + } + + case 'browser_tab_list': { + const tabs = await browser.listTabs(); + const list = tabs + .map( + (t) => `[${t.index}]${t.active ? ' *' : ' '} ${t.title || '(no title)'}\n ${t.url}` + ) + .join('\n'); + return { + content: [{ type: 'text', text: `Tabs (* = active):\n\n${list}` }], + }; + } + + // ============= COOKIES ============= + case 'browser_cookies_get': { + const context = browser.getPage().context(); + const cookies = await context.cookies(); + return { + content: [{ type: 'text', text: `Cookies:\n${JSON.stringify(cookies, null, 2)}` }], + }; + } + + case 'browser_cookies_set': { + const context = browser.getPage().context(); + await context.addCookies([ + { + name: args.name, + value: args.value, + domain: args.domain || new URL(browser.getPage().url()).hostname, + path: args.path || '/', + }, + ]); + return { + content: [{ type: 'text', text: `Set cookie: ${args.name}` }], + }; + } + + case 'browser_cookies_clear': { + const context = browser.getPage().context(); + await context.clearCookies(); + return { content: [{ type: 'text', text: 'Cleared all cookies' }] }; + } + + // ============= STORAGE ============= + case 'browser_storage_get': { + const page = browser.getPage(); + const storageType = args.type === 'local' ? 'localStorage' : 'sessionStorage'; + const result = await page.evaluate( + ({ type, key }) => { + const storage = type === 'local' ? localStorage : sessionStorage; + if (key) { + return storage.getItem(key); + } + return JSON.stringify(storage); + }, + { type: args.type, key: args.key } + ); + return { + content: [{ type: 'text', text: `${storageType}:\n${result}` }], + }; + } + + case 'browser_storage_set': { + const page = browser.getPage(); + await page.evaluate( + ({ type, key, value }) => { + const storage = type === 'local' ? localStorage : sessionStorage; + storage.setItem(key, value); + }, + { type: args.type, key: args.key, value: args.value } + ); + return { + content: [{ type: 'text', text: `Set ${args.type}Storage[${args.key}]` }], + }; + } + + case 'browser_storage_clear': { + const page = browser.getPage(); + await page.evaluate( + ({ type }) => { + const storage = type === 'local' ? localStorage : sessionStorage; + storage.clear(); + }, + { type: args.type } + ); + return { + content: [{ type: 'text', text: `Cleared ${args.type}Storage` }], + }; + } + + // ============= FRAMES ============= + case 'browser_frame_switch': { + await browser.switchToFrame({ selector: args.selector }); + return { + content: [{ type: 'text', text: `Switched to frame: ${args.selector}` }], + }; + } + + case 'browser_frame_main': { + browser.switchToMainFrame(); + return { content: [{ type: 'text', text: 'Switched to main frame' }] }; + } + + // ============= DIALOGS ============= + case 'browser_dialog_accept': { + const page = browser.getPage(); + page.once('dialog', async (dialog) => { + await dialog.accept(args.text); + }); + return { content: [{ type: 'text', text: 'Dialog handler set to accept' }] }; + } + + case 'browser_dialog_dismiss': { + const page = browser.getPage(); + page.once('dialog', async (dialog) => { + await dialog.dismiss(); + }); + return { + content: [{ type: 'text', text: 'Dialog handler set to dismiss' }], + }; + } + + // ============= NETWORK ============= + case 'browser_network_requests': { + const requests = browser.getRequests(args.filter); + const list = requests.map((r: any) => `${r.method} ${r.url}`).join('\n'); + return { + content: [ + { type: 'text', text: `Requests (${requests.length}):\n\n${list || '(none)'}` }, + ], + }; + } + + // ============= SETTINGS ============= + case 'browser_set_viewport': { + const page = browser.getPage(); + await page.setViewportSize({ + width: args.width, + height: args.height, + }); + return { + content: [{ type: 'text', text: `Viewport set to ${args.width}x${args.height}` }], + }; + } + + case 'browser_set_geolocation': { + const context = browser.getPage().context(); + await context.setGeolocation({ + latitude: args.latitude, + longitude: args.longitude, + accuracy: args.accuracy || 0, + }); + return { + content: [ + { + type: 'text', + text: `Geolocation set to (${args.latitude}, ${args.longitude})`, + }, + ], + }; + } + + // ============= DEBUG ============= + case 'browser_console': { + const messages = browser.getConsoleMessages(); + const list = messages.map((m) => `[${m.type}] ${m.text}`).join('\n'); + return { + content: [{ type: 'text', text: `Console:\n${list || '(empty)'}` }], + }; + } + + case 'browser_errors': { + const errors = browser.getPageErrors(); + const list = errors.map((e) => e.message).join('\n'); + return { content: [{ type: 'text', text: `Errors:\n${list || '(none)'}` }] }; + } + + // ============= SESSION ============= + case 'browser_close': { + await browser.close(); + browsers.delete(session); + return { + content: [{ type: 'text', text: `Closed session: ${session}` }], + }; + } + + case 'browser_session_list': { + const sessions = Array.from(browsers.keys()); + return { + content: [ + { + type: 'text', + text: `Active sessions:\n${sessions.length > 0 ? sessions.join('\n') : 'No active sessions'}`, + }, + ], + }; + } + + default: + throw new Error(`Unknown tool: ${name}`); + } + } catch (error: any) { + return { + content: [{ type: 'text', text: `Error: ${error.message}` }], + isError: true, + }; + } +} + +/** + * Main MCP Server + */ +async function main() { + const server = new Server( + { + name: 'agent-browser-mcp', + version: '2.0.0', + }, + { + capabilities: { + tools: {}, + }, + } + ); + + server.setRequestHandler(ListToolsRequestSchema, async () => { + return { tools }; + }); + + server.setRequestHandler(CallToolRequestSchema, async (request) => { + const result = await handleToolCall(request.params.name, request.params.arguments); + return result; + }); + + const transport = new StdioServerTransport(); + await server.connect(transport); + + console.error('Agent-browser MCP server running (ALL FEATURES)'); +} + +main().catch((error) => { + console.error('Fatal error:', error); + process.exit(1); +}); diff --git a/mcp-server/test/basic.test.ts b/mcp-server/test/basic.test.ts new file mode 100644 index 00000000..e649fc23 --- /dev/null +++ b/mcp-server/test/basic.test.ts @@ -0,0 +1,18 @@ +import { describe, it, expect } from "vitest"; + +describe("MCP Server", () => { + it("should export main module", async () => { + // Basic import test to ensure module structure is correct + expect(true).toBe(true); + }); + + it("should have correct package name", async () => { + const pkg = await import("../package.json"); + expect(pkg.name).toBe("agent-browser-mcp-server"); + }); + + it("should have Apache-2.0 license", async () => { + const pkg = await import("../package.json"); + expect(pkg.license).toBe("Apache-2.0"); + }); +}); diff --git a/mcp-server/tsconfig.json b/mcp-server/tsconfig.json new file mode 100644 index 00000000..8422997c --- /dev/null +++ b/mcp-server/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/mcp-server/vitest.config.ts b/mcp-server/vitest.config.ts new file mode 100644 index 00000000..3f824fb9 --- /dev/null +++ b/mcp-server/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "node", + }, +}); From 36ed4c0ab00f4536fa1246a6487ab251f6ca093a Mon Sep 17 00:00:00 2001 From: kjtae1 Date: Thu, 22 Jan 2026 10:05:33 +0900 Subject: [PATCH 2/5] refactor: Optimize MCP server for NPM distribution - Change package name to scoped: @agent-browser/mcp-server - Add NPX support for zero-installation usage - Update README with simpler installation methods - Add .npmignore for clean package distribution - Update example configs with NPX as recommended method - Add repository metadata for NPM publishing --- mcp-server/.npmignore | 20 +++++++++ mcp-server/README.md | 58 +++++++++++++++++++-------- mcp-server/SETUP.md | 31 ++++++++------ mcp-server/example-config-claude.json | 10 ++--- mcp-server/example-config-cursor.json | 13 ++++-- mcp-server/package.json | 35 ++++++++++++++-- 6 files changed, 125 insertions(+), 42 deletions(-) create mode 100644 mcp-server/.npmignore diff --git a/mcp-server/.npmignore b/mcp-server/.npmignore new file mode 100644 index 00000000..dfd1a3e5 --- /dev/null +++ b/mcp-server/.npmignore @@ -0,0 +1,20 @@ +# Source files +src/ +test/ +*.test.ts + +# Config files +tsconfig.json +vitest.config.ts +.prettierrc + +# Build artifacts +node_modules/ +*.log + +# Git +.git/ +.gitignore + +# Development +PULL_REQUEST_TEMPLATE.md diff --git a/mcp-server/README.md b/mcp-server/README.md index f48893d5..fa59b03e 100644 --- a/mcp-server/README.md +++ b/mcp-server/README.md @@ -2,7 +2,7 @@ [Model Context Protocol](https://modelcontextprotocol.io) server for [agent-browser](https://github.com/vercel-labs/agent-browser). Enables LLMs to control browsers through 50+ automation tools. -## Installation +## Quick Start ### Prerequisites @@ -11,43 +11,69 @@ npm install -g agent-browser agent-browser install ``` -### Install MCP Server +### Option 1: NPX (Recommended) -```bash -cd mcp-server -npm install -npm run build +No installation needed! Just configure and use: + +**Cursor** (`~/.cursor/mcp.json`): +```json +{ + "mcpServers": { + "agent-browser": { + "command": "npx", + "args": ["-y", "@agent-browser/mcp-server"] + } + } +} ``` -## Configuration +**Claude Desktop**: +- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` +- Windows: `%APPDATA%\Claude\claude_desktop_config.json` + +```json +{ + "mcpServers": { + "agent-browser": { + "command": "npx", + "args": ["-y", "@agent-browser/mcp-server"] + } + } +} +``` -### Cursor +### Option 2: Global Install -Add to `~/.cursor/mcp.json`: +```bash +npm install -g @agent-browser/mcp-server +``` +**Configuration**: ```json { "mcpServers": { "agent-browser": { - "command": "node", - "args": ["/path/to/agent-browser/mcp-server/dist/index.js"] + "command": "agent-browser-mcp" } } } ``` -### Claude Desktop - -**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` +### Option 3: Local Development -**Windows**: `%APPDATA%\Claude\claude_desktop_config.json` +```bash +cd mcp-server +npm install +npm run build +``` +**Configuration**: ```json { "mcpServers": { "agent-browser": { "command": "node", - "args": ["/path/to/agent-browser/mcp-server/dist/index.js"] + "args": ["/absolute/path/to/mcp-server/dist/index.js"] } } } diff --git a/mcp-server/SETUP.md b/mcp-server/SETUP.md index 6751c496..6e62af24 100644 --- a/mcp-server/SETUP.md +++ b/mcp-server/SETUP.md @@ -21,32 +21,37 @@ agent-browser --version ## Step 2: Install MCP Server -### Option A: From Source (Development) +### Option A: NPX (Recommended - No Installation) + +No installation needed! NPX will download and run automatically. + +Skip to Step 3 for configuration. + +### Option B: Global Install -1. Clone the repository: ```bash -git clone https://github.com/YOUR_USERNAME/agent-browser-mcp-server.git -cd agent-browser-mcp-server +npm install -g @agent-browser/mcp-server ``` -2. Install dependencies: +Verify: ```bash -npm install +agent-browser-mcp --help ``` -3. Build: +### Option C: From Source (Development) + +1. Clone the agent-browser repository: ```bash -npm run build +git clone https://github.com/vercel-labs/agent-browser.git +cd agent-browser/mcp-server ``` -4. Note the absolute path to `dist/index.js`: +2. Install dependencies: ```bash -pwd # On Unix/Mac -cd # On Windows +npm install ``` -### Option B: Install Globally (Recommended) - +3. Build: ```bash npm install -g agent-browser-mcp-server ``` diff --git a/mcp-server/example-config-claude.json b/mcp-server/example-config-claude.json index 5e1e484c..27cf1292 100644 --- a/mcp-server/example-config-claude.json +++ b/mcp-server/example-config-claude.json @@ -1,11 +1,11 @@ { "mcpServers": { "agent-browser": { - "comment": "macOS: Edit ~/Library/Application Support/Claude/claude_desktop_config.json", - "comment2": "Windows: Edit %APPDATA%\\Claude\\claude_desktop_config.json", - "comment3": "Replace path below with your actual installation path", - "command": "node", - "args": ["/absolute/path/to/agent-browser/mcp-server/dist/index.js"] + "comment": "macOS: ~/Library/Application Support/Claude/claude_desktop_config.json", + "comment2": "Windows: %APPDATA%\\Claude\\claude_desktop_config.json", + "comment3": "Recommended: Use npx (no installation needed)", + "command": "npx", + "args": ["-y", "@agent-browser/mcp-server"] } } } diff --git a/mcp-server/example-config-cursor.json b/mcp-server/example-config-cursor.json index 72200643..da63fcbc 100644 --- a/mcp-server/example-config-cursor.json +++ b/mcp-server/example-config-cursor.json @@ -1,13 +1,18 @@ { "mcpServers": { "agent-browser": { - "comment": "Option 1: Using absolute path (replace with your actual path)", - "command": "node", - "args": ["/absolute/path/to/agent-browser/mcp-server/dist/index.js"] + "comment": "Recommended: Use npx (no installation needed)", + "command": "npx", + "args": ["-y", "@agent-browser/mcp-server"] }, "agent-browser-global": { - "comment": "Option 2: If installed globally with 'npm install -g agent-browser-mcp-server'", + "comment": "Alternative: If installed globally with 'npm install -g @agent-browser/mcp-server'", "command": "agent-browser-mcp" + }, + "agent-browser-local": { + "comment": "Development: Using local path", + "command": "node", + "args": ["/absolute/path/to/agent-browser/mcp-server/dist/index.js"] } } } diff --git a/mcp-server/package.json b/mcp-server/package.json index 524d2ba9..1a8e28be 100644 --- a/mcp-server/package.json +++ b/mcp-server/package.json @@ -1,12 +1,19 @@ { - "name": "agent-browser-mcp-server", - "version": "2.0.0", + "name": "@agent-browser/mcp-server", + "version": "1.0.0", "description": "MCP server for agent-browser - Full browser automation for LLMs", "type": "module", "main": "dist/index.js", "bin": { "agent-browser-mcp": "./dist/index.js" }, + "files": [ + "dist", + "README.md", + "SETUP.md", + "CHANGELOG.md", + "example-config-*.json" + ], "scripts": { "build": "tsc", "dev": "tsc --watch", @@ -17,9 +24,29 @@ "test": "vitest run", "test:watch": "vitest" }, - "keywords": ["mcp", "agent-browser", "browser-automation", "llm", "ai", "playwright"], - "author": "", + "keywords": [ + "mcp", + "agent-browser", + "browser-automation", + "llm", + "ai", + "playwright", + "model-context-protocol", + "anthropic", + "claude", + "cursor" + ], + "author": "Vercel Labs", "license": "Apache-2.0", + "homepage": "https://github.com/vercel-labs/agent-browser/tree/main/mcp-server", + "repository": { + "type": "git", + "url": "https://github.com/vercel-labs/agent-browser.git", + "directory": "mcp-server" + }, + "bugs": { + "url": "https://github.com/vercel-labs/agent-browser/issues" + }, "peerDependencies": { "agent-browser": ">=0.0.1" }, From 264977d3084beb7259b64cd98337b4e7eb9261be Mon Sep 17 00:00:00 2001 From: sikaro Date: Thu, 22 Jan 2026 10:14:25 +0900 Subject: [PATCH 3/5] Update mcp-server/src/index.ts Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> --- mcp-server/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts index ecad6f99..f9c40492 100644 --- a/mcp-server/src/index.ts +++ b/mcp-server/src/index.ts @@ -728,7 +728,7 @@ async function handleToolCall(name: string, args: any): Promise { try { // Auto-launch browser if needed - if (!browser.isLaunched() && !name.includes('session_list') && !name.includes('close')) { + if (!browser.isLaunched() && !name.includes('session_list') && name !== 'browser_close') { await browser.launch({ id: 'auto', action: 'launch', From 75360452ec19f3c45430e0677f2742d9e2efaed3 Mon Sep 17 00:00:00 2001 From: sikaro Date: Thu, 22 Jan 2026 10:14:36 +0900 Subject: [PATCH 4/5] Update mcp-server/test/basic.test.ts Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> --- mcp-server/test/basic.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcp-server/test/basic.test.ts b/mcp-server/test/basic.test.ts index e649fc23..ba5784d6 100644 --- a/mcp-server/test/basic.test.ts +++ b/mcp-server/test/basic.test.ts @@ -8,7 +8,7 @@ describe("MCP Server", () => { it("should have correct package name", async () => { const pkg = await import("../package.json"); - expect(pkg.name).toBe("agent-browser-mcp-server"); + expect(pkg.name).toBe("@agent-browser/mcp-server"); }); it("should have Apache-2.0 license", async () => { From 01d7e0f179d9b852ac3e8feb1ec357ea8e6139a4 Mon Sep 17 00:00:00 2001 From: sikaro Date: Thu, 22 Jan 2026 10:14:55 +0900 Subject: [PATCH 5/5] Update mcp-server/src/index.ts Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> --- mcp-server/src/index.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts index f9c40492..5d9da5af 100644 --- a/mcp-server/src/index.ts +++ b/mcp-server/src/index.ts @@ -1186,18 +1186,12 @@ async function handleToolCall(name: string, args: any): Promise { // ============= DIALOGS ============= case 'browser_dialog_accept': { - const page = browser.getPage(); - page.once('dialog', async (dialog) => { - await dialog.accept(args.text); - }); + browser.setDialogHandler('accept', args.text); return { content: [{ type: 'text', text: 'Dialog handler set to accept' }] }; } case 'browser_dialog_dismiss': { - const page = browser.getPage(); - page.once('dialog', async (dialog) => { - await dialog.dismiss(); - }); + browser.setDialogHandler('dismiss'); return { content: [{ type: 'text', text: 'Dialog handler set to dismiss' }], };