
Browser Use
- 15 installs
- 1 repo stars
- Updated January 27, 2026
- bilalmk/todo_correct
browser-use is a skill that automates browser interactions through a Playwright MCP server so an agent can navigate, fill forms, screenshot, and extract data.
About
browser-use is a skill for browser automation through a Playwright MCP server. It lets an agent navigate websites, take accessibility snapshots, click and type using element refs, fill forms, run JavaScript, and take screenshots. A developer uses it for web browsing, form submission, web scraping, or UI testing tasks. It manages the Playwright MCP server lifecycle with helper scripts and requires a shared browser context to keep state across calls.
- Automates browser interactions through a Playwright MCP server
- Navigate, snapshot, click, fill forms, screenshot, and extract data
- Includes start/stop scripts and an mcp-client helper for tool calls
Browser Use by the numbers
- 15 all-time installs (skills.sh)
- Ranked #1,404 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
browser-use capabilities & compatibility
free
- Capabilities
- browser automation · web scraping · form automation · ui testing
- Works with
- playwright
- Use cases
- web scraping · testing · web search
- Runs
- Runs locally
- Pricing
- Free
What browser-use says it does
Browser automation using Playwright MCP. Navigate websites, fill forms, click elements, take screenshots, and extract data.
The `--shared-browser-context` flag is required to maintain browser state across multiple mcp-client.py calls.
Use `ref` from snapshot output to target elements:
npx skills add https://github.com/bilalmk/todo_correct --skill browser-useAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 15 |
|---|---|
| repo stars | ★ 1 |
| Last updated | January 27, 2026 |
| Repository | bilalmk/todo_correct ↗ |
What it does
Give an agent Playwright-driven browser automation for navigation, form submission, scraping, and UI testing.
Who is it for?
Agent-driven web browsing, form filling, scraping, and UI testing
Skip if: Tasks needing no browser interaction
When should I use this skill?
Tasks require web browsing, form submission, web scraping, or UI testing
What you get
A running Playwright MCP browser the agent can navigate, fill, screenshot, and scrape.
- Running Playwright MCP browser session
- Extracted page data and screenshots
By the numbers
- Playwright MCP server runs on port 8808
- Documented workflows for form submission and data extraction
Files
Browser Automation
Automate browser interactions via Playwright MCP server.
Server Lifecycle
Start Server
# Using helper script (recommended)
bash scripts/start-server.sh
# Or manually
npx @playwright/mcp@latest --port 8808 --shared-browser-context &Stop Server
# Using helper script (closes browser first)
bash scripts/stop-server.sh
# Or manually
python3 scripts/mcp-client.py call -u http://localhost:8808 -t browser_close -p '{}'
pkill -f "@playwright/mcp"When to Stop
- End of task: Stop when browser work is complete
- Long sessions: Keep running if doing multiple browser tasks
- Errors: Stop and restart if browser becomes unresponsive
Important: The --shared-browser-context flag is required to maintain browser state across multiple mcp-client.py calls. Without it, each call gets a fresh browser context.
Quick Reference
Navigation
# Go to URL
python3 scripts/mcp-client.py call -u http://localhost:8808 -t browser_navigate \
-p '{"url": "https://example.com"}'
# Go back
python3 scripts/mcp-client.py call -u http://localhost:8808 -t browser_navigate_back -p '{}'Get Page State
# Accessibility snapshot (returns element refs for clicking/typing)
python3 scripts/mcp-client.py call -u http://localhost:8808 -t browser_snapshot -p '{}'
# Screenshot
python3 scripts/mcp-client.py call -u http://localhost:8808 -t browser_take_screenshot \
-p '{"type": "png", "fullPage": true}'Interact with Elements
Use ref from snapshot output to target elements:
# Click element
python3 scripts/mcp-client.py call -u http://localhost:8808 -t browser_click \
-p '{"element": "Submit button", "ref": "e42"}'
# Type text
python3 scripts/mcp-client.py call -u http://localhost:8808 -t browser_type \
-p '{"element": "Search input", "ref": "e15", "text": "hello world", "submit": true}'
# Fill form (multiple fields)
python3 scripts/mcp-client.py call -u http://localhost:8808 -t browser_fill_form \
-p '{"fields": [{"ref": "e10", "value": "john@example.com"}, {"ref": "e12", "value": "password123"}]}'
# Select dropdown
python3 scripts/mcp-client.py call -u http://localhost:8808 -t browser_select_option \
-p '{"element": "Country dropdown", "ref": "e20", "values": ["US"]}'Wait for Conditions
# Wait for text to appear
python3 scripts/mcp-client.py call -u http://localhost:8808 -t browser_wait_for \
-p '{"text": "Success"}'
# Wait for time (ms)
python3 scripts/mcp-client.py call -u http://localhost:8808 -t browser_wait_for \
-p '{"time": 2000}'Execute JavaScript
python3 scripts/mcp-client.py call -u http://localhost:8808 -t browser_evaluate \
-p '{"function": "return document.title"}'Multi-Step Playwright Code
For complex workflows, use browser_run_code to run multiple actions in one call:
python3 scripts/mcp-client.py call -u http://localhost:8808 -t browser_run_code \
-p '{"code": "async (page) => { await page.goto(\"https://example.com\"); await page.click(\"text=Learn more\"); return await page.title(); }"}'Tip: Use browser_run_code for complex multi-step operations that should be atomic (all-or-nothing).
Workflow: Form Submission
1. Navigate to page 2. Get snapshot to find element refs 3. Fill form fields using refs 4. Click submit 5. Wait for confirmation 6. Screenshot result
Workflow: Data Extraction
1. Navigate to page 2. Get snapshot (contains text content) 3. Use browser_evaluate for complex extraction 4. Process results
Tool Reference
See references/playwright-tools.md for complete tool documentation.
Troubleshooting
| Issue | Solution |
|---|---|
| Element not found | Run browser_snapshot first to get current refs |
| Click fails | Try browser_hover first, then click |
| Form not submitting | Use "submit": true with browser_type |
| Page not loading | Increase wait time or use browser_wait_for |
MCP Server Tools
22 tools available
browser_close
Close the page
Flags: destructive
<details> <summary>Full Schema</summary>
{
"type": "object",
"properties": {},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}</details>
browser_resize
Resize the browser window
Flags: destructive
Parameters
- `width` (
number) (required): Width of the browser window - `height` (
number) (required): Height of the browser window
<details> <summary>Full Schema</summary>
{
"type": "object",
"properties": {
"width": {
"type": "number",
"description": "Width of the browser window"
},
"height": {
"type": "number",
"description": "Height of the browser window"
}
},
"required": [
"width",
"height"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}</details>
browser_console_messages
Returns all console messages
Flags: read-only
Parameters
- `level` (
string): Level of the console messages to return. Each level includes the messages of more severe levels. Defaults to "info".
<details> <summary>Full Schema</summary>
{
"type": "object",
"properties": {
"level": {
"type": "string",
"enum": [
"error",
"warning",
"info",
"debug"
],
"default": "info",
"description": "Level of the console messages to return. Each level includes the messages of more severe levels. Defaults to \"info\"."
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}</details>
browser_handle_dialog
Handle a dialog
Flags: destructive
Parameters
- `accept` (
boolean) (required): Whether to accept the dialog. - `promptText` (
string): The text of the prompt in case of a prompt dialog.
<details> <summary>Full Schema</summary>
{
"type": "object",
"properties": {
"accept": {
"type": "boolean",
"description": "Whether to accept the dialog."
},
"promptText": {
"type": "string",
"description": "The text of the prompt in case of a prompt dialog."
}
},
"required": [
"accept"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}</details>
browser_evaluate
Evaluate JavaScript expression on page or element
Flags: destructive
Parameters
- `function` (
string) (required): () => { / code / } or (element) => { / code / } when element is provided - `element` (
string): Human-readable element description used to obtain permission to interact with the element - `ref` (
string): Exact target element reference from the page snapshot
<details> <summary>Full Schema</summary>
{
"type": "object",
"properties": {
"function": {
"type": "string",
"description": "() => { /* code */ } or (element) => { /* code */ } when element is provided"
},
"element": {
"type": "string",
"description": "Human-readable element description used to obtain permission to interact with the element"
},
"ref": {
"type": "string",
"description": "Exact target element reference from the page snapshot"
}
},
"required": [
"function"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}</details>
browser_file_upload
Upload one or multiple files
Flags: destructive
Parameters
- `paths` (
array): The absolute paths to the files to upload. Can be single file or multiple files. If omitted, file chooser is cancelled.
<details> <summary>Full Schema</summary>
{
"type": "object",
"properties": {
"paths": {
"type": "array",
"items": {
"type": "string"
},
"description": "The absolute paths to the files to upload. Can be single file or multiple files. If omitted, file chooser is cancelled."
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}</details>
browser_fill_form
Fill multiple form fields
Flags: destructive
Parameters
- `fields` (
array) (required): Fields to fill in
<details> <summary>Full Schema</summary>
{
"type": "object",
"properties": {
"fields": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Human-readable field name"
},
"type": {
"type": "string",
"enum": [
"textbox",
"checkbox",
"radio",
"combobox",
"slider"
],
"description": "Type of the field"
},
"ref": {
"type": "string",
"description": "Exact target field reference from the page snapshot"
},
"value": {
"type": "string",
"description": "Value to fill in the field. If the field is a checkbox, the value should be `true` or `false`. If the field is a combobox, the value should be the text of the option."
}
},
"required": [
"name",
"type",
"ref",
"value"
],
"additionalProperties": false
},
"description": "Fields to fill in"
}
},
"required": [
"fields"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}</details>
browser_install
Install the browser specified in the config. Call this if you get an error about the browser not being installed.
Flags: destructive
<details> <summary>Full Schema</summary>
{
"type": "object",
"properties": {},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}</details>
browser_press_key
Press a key on the keyboard
Flags: destructive
Parameters
- `key` (
string) (required): Name of the key to press or a character to generate, such asArrowLeftora
<details> <summary>Full Schema</summary>
{
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "Name of the key to press or a character to generate, such as `ArrowLeft` or `a`"
}
},
"required": [
"key"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}</details>
browser_type
Type text into editable element
Flags: destructive
Parameters
- `element` (
string) (required): Human-readable element description used to obtain permission to interact with the element - `ref` (
string) (required): Exact target element reference from the page snapshot - `text` (
string) (required): Text to type into the element - `submit` (
boolean): Whether to submit entered text (press Enter after) - `slowly` (
boolean): Whether to type one character at a time. Useful for triggering key handlers in the page. By default entire text is filled in at once.
<details> <summary>Full Schema</summary>
{
"type": "object",
"properties": {
"element": {
"type": "string",
"description": "Human-readable element description used to obtain permission to interact with the element"
},
"ref": {
"type": "string",
"description": "Exact target element reference from the page snapshot"
},
"text": {
"type": "string",
"description": "Text to type into the element"
},
"submit": {
"type": "boolean",
"description": "Whether to submit entered text (press Enter after)"
},
"slowly": {
"type": "boolean",
"description": "Whether to type one character at a time. Useful for triggering key handlers in the page. By default entire text is filled in at once."
}
},
"required": [
"element",
"ref",
"text"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}</details>
browser_navigate
Navigate to a URL
Flags: destructive
Parameters
- `url` (
string) (required): The URL to navigate to
<details> <summary>Full Schema</summary>
{
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "The URL to navigate to"
}
},
"required": [
"url"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}</details>
browser_navigate_back
Go back to the previous page
Flags: destructive
<details> <summary>Full Schema</summary>
{
"type": "object",
"properties": {},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}</details>
browser_network_requests
Returns all network requests since loading the page
Flags: read-only
Parameters
- `includeStatic` (
boolean): Whether to include successful static resources like images, fonts, scripts, etc. Defaults to false.
<details> <summary>Full Schema</summary>
{
"type": "object",
"properties": {
"includeStatic": {
"type": "boolean",
"default": false,
"description": "Whether to include successful static resources like images, fonts, scripts, etc. Defaults to false."
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}</details>
browser_run_code
Run Playwright code snippet
Flags: destructive
Parameters
- `code` (
string) (required): A JavaScript function containing Playwright code to execute. It will be invoked with a single argument, page, which you can use for any page interaction. For example:async (page) => { await page.getByRole('button', { name: 'Submit' }).click(); return await page.title(); }
<details> <summary>Full Schema</summary>
{
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "A JavaScript function containing Playwright code to execute. It will be invoked with a single argument, page, which you can use for any page interaction. For example: `async (page) => { await page.getByRole('button', { name: 'Submit' }).click(); return await page.title(); }`"
}
},
"required": [
"code"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}</details>
browser_take_screenshot
Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.
Flags: read-only
Parameters
- `type` (
string): Image format for the screenshot. Default is png. - `filename` (
string): File name to save the screenshot to. Defaults topage-{timestamp}.{png|jpeg}if not specified. Prefer relative file names to stay within the output directory. - `element` (
string): Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too. - `ref` (
string): Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too. - `fullPage` (
boolean): When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Cannot be used with element screenshots.
<details> <summary>Full Schema</summary>
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"png",
"jpeg"
],
"default": "png",
"description": "Image format for the screenshot. Default is png."
},
"filename": {
"type": "string",
"description": "File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified. Prefer relative file names to stay within the output directory."
},
"element": {
"type": "string",
"description": "Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too."
},
"ref": {
"type": "string",
"description": "Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too."
},
"fullPage": {
"type": "boolean",
"description": "When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Cannot be used with element screenshots."
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}</details>
browser_snapshot
Capture accessibility snapshot of the current page, this is better than screenshot
Flags: read-only
Parameters
- `filename` (
string): Save snapshot to markdown file instead of returning it in the response.
<details> <summary>Full Schema</summary>
{
"type": "object",
"properties": {
"filename": {
"type": "string",
"description": "Save snapshot to markdown file instead of returning it in the response."
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}</details>
browser_click
Perform click on a web page
Flags: destructive
Parameters
- `element` (
string) (required): Human-readable element description used to obtain permission to interact with the element - `ref` (
string) (required): Exact target element reference from the page snapshot - `doubleClick` (
boolean): Whether to perform a double click instead of a single click - `button` (
string): Button to click, defaults to left - `modifiers` (
array): Modifier keys to press
<details> <summary>Full Schema</summary>
{
"type": "object",
"properties": {
"element": {
"type": "string",
"description": "Human-readable element description used to obtain permission to interact with the element"
},
"ref": {
"type": "string",
"description": "Exact target element reference from the page snapshot"
},
"doubleClick": {
"type": "boolean",
"description": "Whether to perform a double click instead of a single click"
},
"button": {
"type": "string",
"enum": [
"left",
"right",
"middle"
],
"description": "Button to click, defaults to left"
},
"modifiers": {
"type": "array",
"items": {
"type": "string",
"enum": [
"Alt",
"Control",
"ControlOrMeta",
"Meta",
"Shift"
]
},
"description": "Modifier keys to press"
}
},
"required": [
"element",
"ref"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}</details>
browser_drag
Perform drag and drop between two elements
Flags: destructive
Parameters
- `startElement` (
string) (required): Human-readable source element description used to obtain the permission to interact with the element - `startRef` (
string) (required): Exact source element reference from the page snapshot - `endElement` (
string) (required): Human-readable target element description used to obtain the permission to interact with the element - `endRef` (
string) (required): Exact target element reference from the page snapshot
<details> <summary>Full Schema</summary>
{
"type": "object",
"properties": {
"startElement": {
"type": "string",
"description": "Human-readable source element description used to obtain the permission to interact with the element"
},
"startRef": {
"type": "string",
"description": "Exact source element reference from the page snapshot"
},
"endElement": {
"type": "string",
"description": "Human-readable target element description used to obtain the permission to interact with the element"
},
"endRef": {
"type": "string",
"description": "Exact target element reference from the page snapshot"
}
},
"required": [
"startElement",
"startRef",
"endElement",
"endRef"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}</details>
browser_hover
Hover over element on page
Flags: destructive
Parameters
- `element` (
string) (required): Human-readable element description used to obtain permission to interact with the element - `ref` (
string) (required): Exact target element reference from the page snapshot
<details> <summary>Full Schema</summary>
{
"type": "object",
"properties": {
"element": {
"type": "string",
"description": "Human-readable element description used to obtain permission to interact with the element"
},
"ref": {
"type": "string",
"description": "Exact target element reference from the page snapshot"
}
},
"required": [
"element",
"ref"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}</details>
browser_select_option
Select an option in a dropdown
Flags: destructive
Parameters
- `element` (
string) (required): Human-readable element description used to obtain permission to interact with the element - `ref` (
string) (required): Exact target element reference from the page snapshot - `values` (
array) (required): Array of values to select in the dropdown. This can be a single value or multiple values.
<details> <summary>Full Schema</summary>
{
"type": "object",
"properties": {
"element": {
"type": "string",
"description": "Human-readable element description used to obtain permission to interact with the element"
},
"ref": {
"type": "string",
"description": "Exact target element reference from the page snapshot"
},
"values": {
"type": "array",
"items": {
"type": "string"
},
"description": "Array of values to select in the dropdown. This can be a single value or multiple values."
}
},
"required": [
"element",
"ref",
"values"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}</details>
browser_tabs
List, create, close, or select a browser tab.
Flags: destructive
Parameters
- `action` (
string) (required): Operation to perform - `index` (
number): Tab index, used for close/select. If omitted for close, current tab is closed.
<details> <summary>Full Schema</summary>
{
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": [
"list",
"new",
"close",
"select"
],
"description": "Operation to perform"
},
"index": {
"type": "number",
"description": "Tab index, used for close/select. If omitted for close, current tab is closed."
}
},
"required": [
"action"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}</details>
browser_wait_for
Wait for text to appear or disappear or a specified time to pass
Flags: read-only
Parameters
- `time` (
number): The time to wait in seconds - `text` (
string): The text to wait for - `textGone` (
string): The text to wait for to disappear
<details> <summary>Full Schema</summary>
{
"type": "object",
"properties": {
"time": {
"type": "number",
"description": "The time to wait in seconds"
},
"text": {
"type": "string",
"description": "The text to wait for"
},
"textGone": {
"type": "string",
"description": "The text to wait for to disappear"
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}</details>
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Universal MCP Client - Bundle with any skill that needs MCP access.
Supports both HTTP and stdio transports for connecting to MCP servers.
Usage:
# List available tools from an HTTP MCP server
python mcp-client.py list --url http://localhost:8080
# List tools from a stdio MCP server
python mcp-client.py list --stdio "npx -y @modelcontextprotocol/server-github"
# Call a tool
python mcp-client.py call --url http://localhost:8080 --tool create_issue \
--params '{"title": "Bug report", "body": "Details..."}'
# Emit tool schemas as markdown (for caching in references/)
python mcp-client.py emit --url http://localhost:8080
# Emit as JSON (for programmatic use)
python mcp-client.py emit --url http://localhost:8080 --format json
"""
import argparse
import json
import subprocess
import sys
import threading
import queue
from typing import Optional, Any
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError
class MCPClientError(Exception):
"""Base exception for MCP client errors."""
pass
class HTTPTransport:
"""MCP client using HTTP transport (streamable HTTP with session support)."""
def __init__(self, url: str, headers: Optional[dict] = None):
url = url.rstrip('/')
# Playwright MCP and other streamable HTTP servers use /mcp endpoint
if not url.endswith('/mcp'):
url = url + '/mcp'
self.url = url
self.headers = headers or {}
self._request_id = 0
self._session_id: Optional[str] = None
self._initialized = False
def _next_id(self) -> int:
self._request_id += 1
return self._request_id
def _ensure_initialized(self):
"""Initialize the session if not already done."""
if self._initialized:
return
payload = {
"jsonrpc": "2.0",
"id": self._next_id(),
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "mcp-client", "version": "1.0.0"}
}
}
data = json.dumps(payload).encode('utf-8')
headers = {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
**self.headers
}
req = Request(self.url, data=data, headers=headers, method='POST')
try:
with urlopen(req, timeout=30) as resp:
# Check for session ID in response headers
self._session_id = resp.headers.get('Mcp-Session-Id')
response = self._parse_response(resp.read().decode('utf-8'))
except HTTPError as e:
body = e.read().decode('utf-8') if e.fp else str(e)
raise MCPClientError(f"HTTP {e.code}: {body}")
except URLError as e:
raise MCPClientError(f"Connection failed: {e.reason}")
if "error" in response:
err = response["error"]
raise MCPClientError(f"Initialize failed: {err.get('message')}")
self._initialized = True
# Send initialized notification
self._send_notification("notifications/initialized")
def _parse_response(self, body: str) -> dict:
"""Parse response body, handling SSE format if needed."""
body = body.strip()
# Handle SSE format (event stream)
if body.startswith('event:') or body.startswith('data:'):
for line in body.split('\n'):
if line.startswith('data:'):
json_data = line[5:].strip()
if json_data:
return json.loads(json_data)
raise MCPClientError("No data in SSE response")
# Regular JSON response
return json.loads(body)
def _send_notification(self, method: str, params: Optional[dict] = None):
"""Send a notification (no response expected)."""
payload = {
"jsonrpc": "2.0",
"method": method,
}
if params:
payload["params"] = params
data = json.dumps(payload).encode('utf-8')
headers = {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
**self.headers
}
if self._session_id:
headers["Mcp-Session-Id"] = self._session_id
req = Request(self.url, data=data, headers=headers, method='POST')
try:
with urlopen(req, timeout=30) as resp:
pass # Notifications don't return data
except (HTTPError, URLError):
pass # Ignore notification errors
def request(self, method: str, params: Optional[dict] = None) -> dict:
"""Send a JSON-RPC request to the MCP server."""
self._ensure_initialized()
payload = {
"jsonrpc": "2.0",
"id": self._next_id(),
"method": method,
}
if params:
payload["params"] = params
data = json.dumps(payload).encode('utf-8')
headers = {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
**self.headers
}
if self._session_id:
headers["Mcp-Session-Id"] = self._session_id
req = Request(self.url, data=data, headers=headers, method='POST')
try:
with urlopen(req, timeout=30) as resp:
response = self._parse_response(resp.read().decode('utf-8'))
except HTTPError as e:
body = e.read().decode('utf-8') if e.fp else str(e)
raise MCPClientError(f"HTTP {e.code}: {body}")
except URLError as e:
raise MCPClientError(f"Connection failed: {e.reason}")
if "error" in response:
err = response["error"]
raise MCPClientError(f"MCP error {err.get('code')}: {err.get('message')}")
return response.get("result", {})
class StdioTransport:
"""MCP client using stdio transport (for local MCP servers)."""
def __init__(self, command: str):
self.command = command
self._request_id = 0
self._process: Optional[subprocess.Popen] = None
self._response_queue: queue.Queue = queue.Queue()
self._reader_thread: Optional[threading.Thread] = None
def _next_id(self) -> int:
self._request_id += 1
return self._request_id
def _start(self):
"""Start the MCP server process."""
if self._process is not None:
return
self._process = subprocess.Popen(
self.command,
shell=True,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1
)
# Start reader thread
self._reader_thread = threading.Thread(target=self._read_responses, daemon=True)
self._reader_thread.start()
# Send initialize request
self._send({
"jsonrpc": "2.0",
"id": self._next_id(),
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "mcp-client", "version": "1.0.0"}
}
})
# Wait for initialize response
try:
resp = self._response_queue.get(timeout=10)
if "error" in resp:
raise MCPClientError(f"Initialize failed: {resp['error']}")
except queue.Empty:
raise MCPClientError("Timeout waiting for server initialization")
# Send initialized notification
self._send({
"jsonrpc": "2.0",
"method": "notifications/initialized"
})
def _read_responses(self):
"""Background thread to read responses from the server."""
while self._process and self._process.poll() is None:
try:
line = self._process.stdout.readline()
if not line:
break
line = line.strip()
if line:
try:
msg = json.loads(line)
# Only queue responses (messages with id), not notifications
if "id" in msg:
self._response_queue.put(msg)
except json.JSONDecodeError:
pass # Ignore non-JSON output
except Exception:
break
def _send(self, message: dict):
"""Send a message to the server."""
if self._process is None:
raise MCPClientError("Process not started")
line = json.dumps(message) + "\n"
self._process.stdin.write(line)
self._process.stdin.flush()
def request(self, method: str, params: Optional[dict] = None) -> dict:
"""Send a JSON-RPC request and wait for response."""
self._start()
req_id = self._next_id()
payload = {
"jsonrpc": "2.0",
"id": req_id,
"method": method,
}
if params:
payload["params"] = params
self._send(payload)
# Wait for response with matching id
try:
while True:
resp = self._response_queue.get(timeout=30)
if resp.get("id") == req_id:
if "error" in resp:
err = resp["error"]
raise MCPClientError(f"MCP error {err.get('code')}: {err.get('message')}")
return resp.get("result", {})
except queue.Empty:
raise MCPClientError(f"Timeout waiting for response to {method}")
def close(self):
"""Shutdown the server process."""
if self._process:
self._process.terminate()
self._process.wait(timeout=5)
self._process = None
class MCPClient:
"""High-level MCP client that works with any transport."""
def __init__(self, transport):
self.transport = transport
def list_tools(self) -> list[dict]:
"""Get list of available tools from the server."""
result = self.transport.request("tools/list")
return result.get("tools", [])
def call_tool(self, name: str, arguments: Optional[dict] = None) -> Any:
"""Call a tool and return the result."""
params = {"name": name}
if arguments:
params["arguments"] = arguments
result = self.transport.request("tools/call", params)
return result
def list_resources(self) -> list[dict]:
"""Get list of available resources."""
result = self.transport.request("resources/list")
return result.get("resources", [])
def list_prompts(self) -> list[dict]:
"""Get list of available prompts."""
result = self.transport.request("prompts/list")
return result.get("prompts", [])
def emit_markdown(tools: list[dict]) -> str:
"""Generate markdown documentation for tools."""
lines = ["# MCP Server Tools\n"]
lines.append(f"*{len(tools)} tools available*\n")
for tool in tools:
name = tool.get("name", "unnamed")
desc = tool.get("description", "No description")
schema = tool.get("inputSchema", {})
annotations = tool.get("annotations", {})
lines.append(f"## `{name}`\n")
lines.append(f"{desc}\n")
# Add annotations if present
if annotations:
flags = []
if annotations.get("readOnlyHint"):
flags.append("read-only")
if annotations.get("destructiveHint"):
flags.append("destructive")
if annotations.get("idempotentHint"):
flags.append("idempotent")
if flags:
lines.append(f"*Flags: {', '.join(flags)}*\n")
# Add input schema
if schema.get("properties"):
lines.append("### Parameters\n")
required = set(schema.get("required", []))
for prop_name, prop_def in schema["properties"].items():
req_marker = " *(required)*" if prop_name in required else ""
prop_type = prop_def.get("type", "any")
prop_desc = prop_def.get("description", "")
lines.append(f"- **`{prop_name}`** (`{prop_type}`){req_marker}: {prop_desc}")
lines.append("")
# Add full schema as collapsible
lines.append("<details>")
lines.append("<summary>Full Schema</summary>\n")
lines.append("```json")
lines.append(json.dumps(schema, indent=2))
lines.append("```")
lines.append("</details>\n")
return "\n".join(lines)
def emit_json(tools: list[dict]) -> str:
"""Generate JSON output for tools."""
return json.dumps({"tools": tools}, indent=2)
def main():
parser = argparse.ArgumentParser(
description="Universal MCP Client - connect to any MCP server",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__
)
# Commands
subparsers = parser.add_subparsers(dest="command", required=True)
# list command
list_parser = subparsers.add_parser("list", help="List available tools")
list_parser.add_argument("--verbose", "-v", action="store_true", help="Show full tool details")
# call command
call_parser = subparsers.add_parser("call", help="Call a tool")
call_parser.add_argument("--tool", "-t", required=True, help="Tool name")
call_parser.add_argument("--params", "-p", default="{}", help="JSON parameters")
# emit command
emit_parser = subparsers.add_parser("emit", help="Emit tool schemas as documentation")
emit_parser.add_argument("--format", "-f", choices=["markdown", "json"], default="markdown")
# resources command
subparsers.add_parser("resources", help="List available resources")
# prompts command
subparsers.add_parser("prompts", help="List available prompts")
# Transport options (added to all subparsers)
for sub in [list_parser, call_parser, emit_parser]:
transport_group = sub.add_mutually_exclusive_group(required=True)
transport_group.add_argument("--url", "-u", help="HTTP URL of MCP server")
transport_group.add_argument("--stdio", "-s", help="Command to start stdio MCP server")
sub.add_argument("--header", "-H", action="append", default=[],
help="HTTP header (format: 'Name: Value')")
args = parser.parse_args()
# Create transport
transport = None
try:
if hasattr(args, 'url') and args.url:
headers = {}
for h in args.header:
if ':' in h:
key, value = h.split(':', 1)
headers[key.strip()] = value.strip()
transport = HTTPTransport(args.url, headers)
elif hasattr(args, 'stdio') and args.stdio:
transport = StdioTransport(args.stdio)
else:
parser.error("Must specify --url or --stdio")
client = MCPClient(transport)
# Execute command
if args.command == "list":
tools = client.list_tools()
if args.verbose:
print(json.dumps(tools, indent=2))
else:
for tool in tools:
desc = tool.get("description", "")[:60]
print(f" {tool['name']}: {desc}...")
elif args.command == "call":
params = json.loads(args.params)
result = client.call_tool(args.tool, params)
print(json.dumps(result, indent=2))
elif args.command == "emit":
tools = client.list_tools()
if args.format == "markdown":
print(emit_markdown(tools))
else:
print(emit_json(tools))
elif args.command == "resources":
resources = client.list_resources()
print(json.dumps(resources, indent=2))
elif args.command == "prompts":
prompts = client.list_prompts()
print(json.dumps(prompts, indent=2))
except MCPClientError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Invalid JSON: {e}", file=sys.stderr)
sys.exit(1)
except KeyboardInterrupt:
sys.exit(130)
finally:
if transport and hasattr(transport, 'close'):
transport.close()
if __name__ == "__main__":
main()
#!/bin/bash
# Start Playwright MCP server for browser-use skill
# Usage: ./start-server.sh [port]
PORT=${1:-8808}
PID_FILE="/tmp/playwright-mcp-${PORT}.pid"
# Check if already running
if [ -f "$PID_FILE" ] && kill -0 $(cat "$PID_FILE") 2>/dev/null; then
echo "Playwright MCP already running on port $PORT (PID: $(cat $PID_FILE))"
exit 0
fi
# Start server
npx @playwright/mcp@latest --port "$PORT" --shared-browser-context &
echo $! > "$PID_FILE"
sleep 2
if kill -0 $(cat "$PID_FILE") 2>/dev/null; then
echo "Playwright MCP started on port $PORT (PID: $(cat $PID_FILE))"
else
echo "Failed to start Playwright MCP"
rm -f "$PID_FILE"
exit 1
fi
#!/bin/bash
# Stop Playwright MCP server
# Usage: ./stop-server.sh [port]
PORT=${1:-8808}
PID_FILE="/tmp/playwright-mcp-${PORT}.pid"
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")
if kill -0 "$PID" 2>/dev/null; then
# First close the browser gracefully
python3 "$(dirname "$0")/mcp-client.py" call -u "http://localhost:${PORT}" -t browser_close -p '{}' 2>/dev/null || true
# Then kill the server
kill "$PID" 2>/dev/null
sleep 1
# Force kill if still running
kill -9 "$PID" 2>/dev/null || true
echo "Playwright MCP stopped (was PID: $PID)"
else
echo "Playwright MCP not running (stale PID file)"
fi
rm -f "$PID_FILE"
else
# Try to find and kill by process name
pkill -f "@playwright/mcp.*--port.*${PORT}" 2>/dev/null && echo "Playwright MCP stopped" || echo "Playwright MCP not running"
fi
Related skills
FAQ
Why is --shared-browser-context required?
It maintains browser state across multiple mcp-client.py calls; without it each call gets a fresh browser context.
How do I target elements to click?
Run browser_snapshot first to get element refs, then pass the ref to browser_click or browser_type.