web-speed-agent
This server lets an AI agent automate a local browser, manage credentials, and extract structured web data via Web Speed API.
Store login credentials securely in the system keychain, locally only.
Set up Chrome/Edge agent profiles and sync cookies from your main browser for pre-authenticated sessions.
Open/connect to Chrome, Firefox, or Edge via CDP, profiles, headless mode, or named cookie-persistent sessions.
Navigate to URLs and optionally verify expected URL substrings or detect SPA redirects.
Log in with keychain credentials or supplied username/password, using auto or custom selectors.
Interact with pages: click elements, fill fields, send real keystrokes, submit forms.
Wait for elements, URL changes, or custom JS predicates to become true.
Evaluate JavaScript in the page for Shadow DOM, iframes, hidden data, visibility checks, or event triggering.
Extract structured data from the current page via
read_page(auto/article/product/listing/other); costs 1 Web Speed credit.Write text into Google Docs or Google Slides and add new Google Slides.
Check Web Speed API credit balance and account status.
Close the agent browser tab/session when done.
Allows extraction of article data from TechCrunch pages using the Web Speed API.
web-speed-agent
Local browser automation + Web Speed API integration for authenticated web extraction.
Point an AI agent at any website — including ones that require login — and get back clean, structured data. Credentials stay on your machine. Only extracted HTML goes to the server.
pip install web-speed-agent
playwright install chromiumNeeds Python 3.10 or newer (3.13 and 3.14 both work). macOS ships 3.9, so a stock
pip3 installfails with a misleadingNo matching distribution found— see troubleshooting. The one-line installer avoids this by fetching a suitable Python for you.
Want to use this with Claude, Gemini, or other AI clients?
Check out the MCP Server Installation Guide — it's the easiest way to let AI agents log in and extract data through natural language.
We also recommend setting up a master instructional file for your LLM of choice (gemini.md, "Instructions for Claude", etc.). A sample doc can be found here
How it works
Your machine Web Speed server
───────────────────────────────── ──────────────────────────
Playwright browser (local)
↓ navigates, logs in, clicks
↓ gets page HTML
↓ (no passwords sent)
agent.extract(html) ────────→ Advanced extraction engine
←──────── Structured JSONCredentials never leave your machine. The server only sees HTML.
Related MCP server: ChatGPT Gateway MCP Nyan
Quickstart
import asyncio
from web_speed_agent import Agent
async def main():
agent = Agent(api_key="wsp_...") # or set WEBSPEED_API_KEY env var
# Public pages — no browser needed
result = await agent.map("https://techcrunch.com/some-article/")
print(result["article"]["sections"])
# Authenticated pages — browser runs locally
agent.store_credential("mysite", "me@example.com", "mypassword")
async with agent.browser(session_name="mysite") as browser:
page = await browser.new_page()
await page.goto("https://mysite.com/login")
username, password = agent.get_credential("mysite")
await page.fill('[name="email"]', username)
await page.fill('[name="password"]', password)
await page.click('button[type="submit"]')
await page.wait_for_load_state("networkidle")
# Now on a logged-in page — extract it
html = await page.content()
result = await agent.extract(html, page_type="listing")
print(result["listing"]["items"])
asyncio.run(main())Get an API key at getwebspeed.io.
Installation
Requirements: Python 3.10 or newer (no upper limit — 3.13 and 3.14 are
verified), and a Web Speed API key. mcp and playwright both require 3.10+, so
this floor is not ours to lower. On macOS check with python3 --version first:
the system Python is 3.9.
pip install web-speed-agent
playwright install chromium
export WEBSPEED_API_KEY="wsp_..."Core concepts
Agent
The main class. Manages credentials, browser sessions, and API calls.
from web_speed_agent import Agent
# API key from argument
agent = Agent(api_key="wsp_...")
# API key from environment variable (recommended)
# export WEBSPEED_API_KEY="wsp_..."
agent = Agent()
# Use as async context manager (auto-closes HTTP client)
async with Agent() as agent:
...Extracting public pages
No browser needed for pages that don't require login:
# Fetch + extract in one call
result = await agent.map("https://example.com/article")
# With JavaScript rendering (for heavy SPAs)
result = await agent.map("https://example.com/spa", js=True)Extracting authenticated pages
Use a local browser session. The browser runs on your machine:
async with agent.browser(session_name="mysite") as browser:
page = await browser.new_page()
await page.goto("https://mysite.com/dashboard")
html = await page.content()
result = await agent.extract(html)The session_name persists cookies to ~/.webspeed/sessions/<name>/ so subsequent runs skip the login step.
Credential management
Credentials are stored in your system keychain (macOS Keychain, Windows Credential Manager, Linux secret-tool). They are never sent to Web Speed servers.
# Store once
agent.store_credential("mysite", "me@example.com", "mypassword")
# Retrieve anywhere
username, password = agent.get_credential("mysite")
# Remove
agent.delete_credential("mysite")Extraction output
The server returns page-type-aware structured data:
# Article
result = await agent.extract(html, page_type="article")
# result["page_type"] → "article"
# result["title"] → "Article Title"
# result["author"] → "Jane Smith"
# result["published_date"] → "2026-05-06"
# result["article"]["sections"] → [{"heading": "...", "paragraphs": [...]}]
# result["article"]["links"] → [{"text": "...", "url": "..."}]
# Product
result = await agent.extract(html, page_type="product")
# result["product"]["name"] → "Wireless Headphones"
# result["product"]["price"] → "$99.99"
# result["product"]["availability"] → "In Stock"
# result["product"]["rating"] → "4.5"
# result["product"]["specs"] → {"Battery": "30h", ...}
# Listing (search results, category pages)
result = await agent.extract(html, page_type="listing")
# result["listing"]["items"] → [{"title": "...", "url": "...", "price": "..."}]
# Auto-detect (default)
result = await agent.extract(html)
# result["page_type"] → "article" | "product" | "listing" | "other"All results include engine: "advanced" — 60–85% more token-efficient than raw HTML.
Examples
Price monitor
import asyncio
from web_speed_agent import Agent
async def check_price(url: str, site_name: str) -> str:
async with Agent() as agent:
agent.store_credential(site_name, "me@example.com", "password", overwrite=True)
async with agent.browser(session_name=site_name) as browser:
page = await browser.new_page()
# Login
await page.goto(f"https://{site_name}.com/login")
user, pwd = agent.get_credential(site_name)
await page.fill('[name="email"]', user)
await page.fill('[name="password"]', pwd)
await page.click('button[type="submit"]')
await page.wait_for_load_state("networkidle")
# Check product
await page.goto(url)
await page.wait_for_load_state("networkidle")
html = await page.content()
result = await agent.extract(html, page_type="product")
return result.get("product", {}).get("price", "unknown")
price = asyncio.run(check_price("https://example.com/product/123", "example"))
print(f"Current price: {price}")Read a private dashboard
import asyncio
from web_speed_agent import Agent
async def get_dashboard_data():
async with Agent() as agent:
async with agent.browser(session_name="analytics") as browser:
page = await browser.new_page()
# Login (first run only — session persists after)
creds = agent.get_credential("analytics")
if not creds:
agent.store_credential("analytics", "me@company.com", "password")
creds = agent.get_credential("analytics")
await page.goto("https://analytics.company.com/login")
await page.fill('[name="email"]', creds[0])
await page.fill('[name="password"]', creds[1])
await page.click('button[type="submit"]')
await page.wait_for_load_state("networkidle")
# Navigate to dashboard
await page.goto("https://analytics.company.com/dashboard")
await page.wait_for_selector(".metrics-table", timeout=10000)
html = await page.content()
result = await agent.extract(html)
return result
asyncio.run(get_dashboard_data())Multi-page scrape while logged in
import asyncio
from web_speed_agent import Agent
async def scrape_inbox():
async with Agent() as agent:
async with agent.browser(session_name="webmail") as browser:
page = await browser.new_page()
# Login
await page.goto("https://mail.example.com/login")
user, pwd = agent.get_credential("webmail")
await page.fill('[name="username"]', user)
await page.fill('[name="password"]', pwd)
await page.click('[type="submit"]')
await page.wait_for_load_state("networkidle")
# Scrape multiple pages
emails = []
for page_num in range(1, 4):
await page.goto(f"https://mail.example.com/inbox?page={page_num}")
await page.wait_for_load_state("networkidle")
html = await page.content()
result = await agent.extract(html, page_type="listing")
emails.extend(result.get("listing", {}).get("items", []))
return emails
asyncio.run(scrape_inbox())AI agent integration (MCP)
The included MCP server lets Claude Desktop, Gemini CLI, and any MCP-compatible agent use the SDK directly. The agent can log in, navigate, click, and extract — all through natural language.
Install and configure (one command, Mac & Linux):
curl -fsSL https://api.getwebspeed.io/install.sh | shIt asks for your API key at a hidden prompt, stores it in your OS keychain (macOS Keychain / Windows Credential Manager / Linux Secret Service), and registers two servers with every client it finds — Claude Desktop, Claude Code, Antigravity:
Entry | Command | What it is |
|
| The hosted mapping tools, relayed to |
|
| The local browser agent |
Neither entry contains your key — both read it from the keychain at startup.
On Windows: py -m pip install --user web-speed-agent then
py -m web_speed_agent.configure --all --apply.
Managing the key:
webspeed-key set # store / replace it (prompts; never pass it as an argument)
webspeed-key status # which key is in use (last 4 chars) and any plaintext copies left behind
webspeed-key deleteAfter rotating your key on the account page, run webspeed-key set on each
machine and restart your AI app. No config changes.
By hand (after webspeed-key set), e.g. Claude Desktop:
{
"mcpServers": {
"web-speed-agent": {
"command": "/absolute/path/to/venv/bin/python",
"args": ["-m", "web_speed_agent.mcp_server"]
}
}
}Then tell the agent:
"Store my credentials for united — username me@example.com, password mypassword"
"Log into united.com and find me the cheapest flight from SFO to JFK next Friday"
Available MCP tools:
Tool | Description |
| Save login to system keychain |
| Open browser + sign in |
| Go to a URL in the active session |
| Get structured data from current page |
| Click a button or link |
| Type into a form field |
| Send real keystrokes to the page (games, canvas editors, shortcuts) |
| Open hover-only menus and tooltips |
| Scroll, or bring an element into view |
| Choose an option in a dropdown |
| Go back in browser history |
| Wait until a JS expression is truthy, instead of sleeping |
| Submit a form |
| End the browser session |
| See which safety controls are active |
| Check API credit balance |
API reference
Agent
Agent(
api_key: str | None = None,
server_url: str | None = None,
config_dir: str = "~/.webspeed",
headless: bool = True,
)Parameter | Description |
| Web Speed API key. Falls back to |
| Override API server URL. Default: |
| Directory for config, sessions, and logs. Default: |
| Run browser headlessly. Default: |
agent.browser()
agent.browser(
session_name: str | None = None,
headless: bool | None = None,
proxy: str | None = None,
) -> ManagedBrowserReturns an async context manager. Inside the block, call .new_page() to get a Playwright Page.
Parameter | Description |
| Persist cookies to |
| Override instance |
| Proxy URL e.g. |
Session names must be alphanumeric + hyphens/underscores, max 64 chars.
agent.extract()
await agent.extract(
html: str,
page_type: str = "auto",
) -> dictSends HTML to the Web Speed API. Costs 1 credit.
Parameter | Description |
| Raw HTML string (e.g. from |
|
|
agent.map()
await agent.map(
url: str,
js: bool = False,
) -> dictFetches and extracts a public URL via the server. No local browser needed. Costs 1 credit.
Parameter | Description |
| Page URL. Must be |
| Render JavaScript before extracting. |
agent.account()
await agent.account() -> dictReturns: credits, tier, status, lifetime (total/hits/misses).
agent.store_credential()
agent.store_credential(
site: str,
username: str,
password: str,
overwrite: bool = False,
) -> NoneSaves to system keychain. Raises CredentialError if credential exists and overwrite=False.
agent.get_credential()
agent.get_credential(site: str) -> tuple[str, str] | NoneReturns (username, password) or None if not found.
agent.delete_credential()
agent.delete_credential(site: str) -> NoneRemoves credential from keychain.
Exceptions
from web_speed_agent import (
WebSpeedError, # Base exception
AuthenticationError, # Invalid/missing API key
InsufficientCreditsError, # No credits remaining
APIError, # API returned 4xx/5xx
RateLimitError, # 429 Too Many Requests
CredentialError, # Keychain error
BrowserError, # Playwright error
NetworkError, # Timeout or DNS failure
PlaywrightNotInstalledError, # Run: playwright install chromium
)from web_speed_agent import Agent, InsufficientCreditsError, NetworkError
try:
result = await agent.extract(html)
except InsufficientCreditsError:
print("Out of credits — top up at getwebspeed.io")
except NetworkError as e:
print(f"Connection failed: {e}")Configuration
Environment variables
Variable | Description |
| API key. Overrides the keychain; use it where there is no keychain (CI, Docker, headless Linux). On a desktop, prefer |
| Override server URL (must be |
Config file
~/.webspeed/config.yaml — created automatically on first run. Permissions set to 0o600 (owner-only).
api:
server_url: https://api.getwebspeed.io
timeout: 30
browser:
headless: trueSession files
Persisted browser sessions are stored in ~/.webspeed/sessions/<name>/storage.json.
Permissions:
0o600(owner-only)Contains: cookies, localStorage, sessionStorage
Safe to delete: agent will re-authenticate on next run
Security
What leaves your machine
When you call agent.extract(html), the page HTML is sent to the Web Speed API for processing. Everything else stays local.
Data | Where it goes |
Login credentials | Never leave your machine (system keychain only) |
Browser cookies / session | Never leave your machine (local Playwright) |
Page HTML | Sent over HTTPS to Web Speed API for extraction |
Extracted JSON | Returned to you |
HTML scrubbing (on by default)
Before any HTML is transmitted, the SDK automatically scrubs it locally:
Inline
<script>and<style>blocks removedHidden form fields with auth-related names (
csrf,token,nonce,session, etc.) have their values blankedSensitive
<meta>content attributes clearedHTML comments removed
Visible content — text, links, tables, headings, product data — is untouched.
# Default: scrubbing is on
result = await agent.extract(html)
# Turn off only if the page has no sensitive data
result = await agent.extract(html, scrub=False)
# Or scrub manually and inspect before sending
from web_speed_agent import scrub
clean_html = scrub(raw_html)
print(clean_html) # inspect what will be sent
result = await agent.extract(clean_html, scrub=False)Server-side data handling
HTML processed in-memory only — never written to disk, never logged, never cached
Auth-gated pages never cached — pages requiring login are explicitly excluded from the shared registry
Usage logs store only: a hash of your API key, a hash of the URL (or
"sdk-extract"), timestamp, and detected page type — no contentNo raw HTML in error responses — exceptions are sanitized before any error is returned
Other protections
Credentials stored in system keychain, never in files, never sent to servers
Session files written with
0o600permissions (owner-only read/write)Config directory created with
0o700permissionsTLS always verified —
verify=Trueon all HTTP calls, cannot be disabledHTTPS enforced —
server_urlmust start withhttps://, plain HTTP rejectedPath traversal prevention — session names validated against
[a-zA-Z0-9_-]allowlistNo credential logging — passwords never appear in logs or error messages
License
GNU General Public License v3.0 — see LICENSE.
Web Speed API usage is subject to the Web Speed Terms of Service.
Available Tools
24 toolsaccount_infoA
Check your Web Speed API credit balance and account status.
Inputs: no named parameters. Returns: result
clickA
Click an element by CSS selector.
The wait arguments all run inside THIS call. Reach for them instead of
following a click with a separate wait tool — the extra round-trip costs far
more than the wait. Prefer wait_for / wait_for_predicate / wait_until
over wait_ms: a fixed sleep is either too short (flaky) or too long (slow),
while a condition returns the moment it is satisfied.
Args:
selector: CSS selector for the element to click.
wait_for_navigation: Wait for a page load after clicking (default True).
Set to False for clicks that trigger in-page UI changes
like modals, dropdowns, or expanding sections.
wait_for: CSS selector to wait for AFTER clicking — use this when the click
opens a modal or triggers async UI rendering. The tool waits up to
5 s for the element to appear before returning.
wait_ms: Fixed sleep after the click. Discouraged — use wait_until or
wait_for_predicate, which finish as soon as the page is ready.
wait_for_predicate: JS expression polled (up to 5 s) until it returns
truthy, e.g. "!document.querySelector('.spinner')".
wait_until: Readiness signal — 'dom_settled' (wait for the DOM to stop
changing; the right replacement for wait_ms), 'networkidle',
'load', 'domcontentloaded', 'settle', or 'none'. Defaults to the
historical behaviour: settle unless wait_for was given.
Waits that time out do not fail the click — the click already happened. They
come back in a warnings list so you can see, for instance, that the modal
you expected never opened.
Inputs: selector (required), wait_ms, wait_for, wait_until, +2 more. Returns: result
close_browserA
Close the tab and disconnect from the browser.
In CDP mode (connected to your existing Chrome): closes the tab the agent opened and disconnects. Chrome itself stays running with all your other tabs.
In standard mode: saves the session (if named) and closes the browser.
Inputs: no named parameters. Returns: result
evaluateA
Run JavaScript in the page context and return the result.
Use this to handle situations standard selectors can't reach:
Shadow DOM: document.querySelector('my-el').shadowRoot.querySelector('input')
Iframes: document.querySelector('iframe').contentDocument.querySelector('p')
Hidden data: window.INITIAL_DATA or JSON.parse(document.getElementById('NEXT_DATA').textContent)
Visibility checks: document.querySelector('.modal')?.getBoundingClientRect()
Triggering events: document.querySelector('input').dispatchEvent(new Event('focus'))
Args: js: JavaScript expression to evaluate. The return value is JSON-serialised and included in the response. Keep expressions simple — complex logic is better split across multiple calls.
Inputs: js (required). Returns: result
fill_fieldA
Type a value into a form field.
Standard mode (use_keyboard=False, default): sets the field value
directly. Works for plain <input> and <textarea> elements.
Keyboard mode (use_keyboard=True): simulates real keystrokes
(keydown → keypress → input → keyup per character). Use this for:
contenteditabledivs (X/Twitter post box, Notion, Slack, etc.)React / Vue inputs that ignore programmatic
.valuechangesSites that check for "trusted" input events to prevent botting
For X (Twitter): click the "What's happening?" box first, then call
fill_field with use_keyboard=True. This fires the React-compatible
events that enable the Post button.
Args: selector: CSS selector for the input or contenteditable element. value: Text to type. Never include \n — it will be stripped. A trailing \n is treated as Tab (advance to next field). press_tab: Press Tab after filling to move focus to the next field. use_keyboard: Simulate real keystrokes instead of direct fill. delay_ms: Milliseconds between keystrokes in keyboard mode. 0 = fast (default). Use 30–80 for sites that check typing cadence.
Inputs: value (required), selector (required), delay_ms, press_tab, +1 more. Returns: result
get_page_infoA
Return the current page URL, title, and visible text snippet.
Useful for orientation — call this to confirm where the browser is.
Inputs: no named parameters. Returns: result
go_backA
Go back in browser history.
The honest way out of a wrong turn. Re-navigating to a remembered URL is not the same thing: it drops scroll position, in-page state and any POST result, and on a SPA the URL you remember may not rebuild the view you were on.
Args: steps: How many entries to go back (1–20). wait_until: Readiness signal after the last step. Defaults to 'settle'.
Inputs: steps, wait_until. Returns: result
hoverA
Move the pointer over an element, without clicking.
Whole menus exist only on hover — nav dropdowns, tooltips, the row of icons
that appears on a table row. A synthetic mouseover event dispatched from
JavaScript often will not open them, because the site listens for a trusted
pointer or checks :hover in CSS. This moves the real pointer.
Args: selector: CSS selector for the element to hover. wait_for: CSS selector to wait for afterwards (the menu that should open). wait_for_predicate: JS expression polled until truthy afterwards. wait_until: Readiness signal afterwards — 'none' (default), 'dom_settled', 'networkidle', 'load', 'domcontentloaded', 'settle'.
Inputs: selector (required), wait_for, wait_until, wait_for_predicate. Returns: result
loginA
Fill a login form and submit it.
Credentials: provide either site (to load from keychain) OR
username + password directly.
Selectors: if omitted, common patterns are tried automatically (input[type=email], input[name=username], etc.).
Use navigate() to go to the login page first.
Inputs: site, password, username, submit_selector, +2 more. Returns: result
open_browserA
Open a browser for automation.
browser — which browser to use: "chrome" (default), "firefox", or "edge".
── Chrome default behaviour ────────────────────────────────────────────────── For Chrome, CDP is tried automatically first. If the user has run 'chrome-agent' (which opens Chrome with --remote-debugging-port=9222), the agent connects to that existing window and opens a new tab — the user's real Chrome with all their logins, cookies, and extensions.
If Chrome is not running with the debug port, a helpful message is returned explaining how to start it.
── Firefox ─────────────────────────────────────────────────────────────────── Imports cookies from the user's Firefox profile (read-only) into a fresh Playwright session. Pass profile_path="auto" to detect the profile.
── Manual overrides ────────────────────────────────────────────────────────── cdp_url: connect to a specific debug URL (e.g. non-default port). profile_path: use an explicit profile directory (Chrome/Firefox/Edge). session_name: persist cookies across fresh Playwright sessions.
Args: browser: "chrome" (default), "firefox", or "edge". session_name: Cookie-persist name for standard/fresh mode. headless: Hide the window in standard/profile mode (default False). cdp_url: Override the CDP URL (default for Chrome: http://localhost:9222). profile_path: Launch with an existing browser profile ("auto" or full path).
Inputs: browser, cdp_url, headless, profile_path, +1 more. Returns: result
press_keysA
Send real keystrokes to the page, rather than into a form field.
fill_field writes into an <input>. This types at whatever holds keyboard
focus, which is what word games, canvas editors (Figma, Excalidraw),
terminal emulators, and keyboard-shortcut UIs actually listen to: they bind
keydown/keyup on window or document, so setting an input's value
never reaches them and there is often no input to target in the first place.
Examples: press_keys(text="crane", keys=["Enter"]) # type a word, submit it press_keys(keys=["ArrowDown"], repeat=3) # move a selection press_keys(keys=["Control+a", "Backspace"]) # select all, clear press_keys(text="hello", selector="#chat") # focus first, then type
Args:
text: Literal text to type one character at a time, firing the full
keydown → keypress → input → keyup sequence per character. Typed
BEFORE keys, so text="crane", keys=["Enter"] does the right thing.
keys: Key names pressed in order — ["Enter"], ["Escape"], ["ArrowLeft"] —
or chords like ["Control+a"], ["Shift+Tab"]. Playwright names:
Enter, Tab, Escape, Backspace, Delete, ArrowUp/Down/Left/Right,
Home, End, PageUp, PageDown, F1–F12, and single characters.
selector: CSS selector to focus first. Omit to send keys to whatever
already has focus — the usual case for games and canvas apps.
delay_ms: Milliseconds per keystroke (0–1000). 0 is fastest; use 20–80
on sites that check typing cadence.
repeat: Press the keys sequence this many times (1–50). Does not
repeat text.
wait_for: CSS selector to wait for afterwards, in this same call.
wait_for_predicate: JS expression polled until truthy afterwards.
wait_until: Readiness signal afterwards — 'none' (default), 'dom_settled'
(wait for the DOM to stop changing; prefer this over a fixed
sleep), 'networkidle', 'load', 'domcontentloaded', 'settle'.
Inputs: keys, text, repeat, delay_ms, +4 more. Returns: result
read_pageA
Extract structured data from the current page via the Web Speed API.
Returns type-aware structured JSON: article → title, author, sections, links product → name, price, availability, specs listing → items with title, url, price, snippet other → headings, navigation, forms, text_blocks
Costs 1 Web Speed credit. Requires WEBSPEED_API_KEY.
Inputs: page_type. Returns: result
safety_statusA
What this Bridge is currently allowed to do.
Call it first when an action is refused, or before planning anything that changes data. The limits are set outside the agent and cannot be changed from here — knowing them up front beats discovering them one failure at a time.
Reports read-only mode, the confirmation level, any site allow/deny lists, and where the audit log is written.
Inputs: no named parameters. Returns: result
scrollA
Scroll the page, or bring an element into view.
Needed for more than reading: infinite feeds only load the next batch once you reach the bottom, and lazy images never request until they approach the viewport. An element below the fold can also be genuinely unclickable.
Args:
to: 'down', 'up', 'top', 'bottom', or 'element' (with selector).
selector: Element to scroll into view. Implies to='element'.
amount_px: Pixels to scroll for 'down'/'up' (default 600).
wait_until: Readiness signal afterwards. Defaults to 'dom_settled', which
is what makes this useful on infinite scroll — it returns once
the newly loaded rows stop arriving.
Inputs: to, selector, amount_px, wait_until. Returns: result
select_optionA
Choose an option in a dropdown.
fill_field cannot do this — typing into a does nothing. Pick ONE
of value / label / index:
value matches the option's `value` attribute (most reliable)
label matches the visible text
index zero-based positionArgs: selector: CSS selector for the element. value: The option's value attribute. label: The option's visible text. index: Zero-based option position.
Inputs: selector (required), index, label, value. Returns: result
setup_browserA
Set up Chrome or Edge for agent use (macOS and Windows).
Run this ONCE (with your browser closed). It:
Creates a dedicated agent profile at ~/.webspeed/chrome-debug/ (macOS) or %LOCALAPPDATA%\WebSpeedAgent\chrome-debug\ (Windows) — a non-default user data directory, which is required by Chrome before it will open the remote debugging port.
Copies your existing Chrome cookies into that profile so you are already logged into all your sites when the agent opens the browser.
macOS: installs ~/bin/chrome-agent and adds a shell alias in ~/.zshrc. Windows: writes chrome-agent.bat to your Desktop.
After setup:
macOS: type 'chrome-agent' in Terminal to open Chrome
Windows: double-click chrome-agent.bat on your Desktop
Tell the agent: open_browser(browser="chrome", cdp_url="http://localhost:9222")
The agent opens a new tab in your real Chrome with all your logins active
Re-run setup_browser() any time you want to sync fresh cookies from your main Chrome profile into the agent profile (close Chrome first).
Args: browser: "chrome" (default) or "edge".
Inputs: browser. Returns: result
store_credentialA
Save login credentials to the system keychain (macOS/Windows/Linux).
Credentials are stored locally and NEVER sent to any server. Use site as a short identifier, e.g. "indiehackers", "twitter", "gmail".
Inputs: site (required), password (required), username (required). Returns: result
submit_formA
Submit a form by clicking a submit button or pressing Enter.
Args: selector: CSS selector of the submit button or form. If omitted, presses Enter on the focused element.
Inputs: selector. Returns: result
wait_for_elementA
Wait for an element to reach a given state on the page.
Useful after an action that triggers async loading, modal opening, or element removal. Returns ok once the condition is met.
Args: selector: CSS selector for the element to watch. timeout_ms: Maximum time to wait in milliseconds (default 10 000). state: One of: 'visible' — element exists and is visible (default) 'hidden' — element exists but is hidden, or does not exist 'attached' — element is in the DOM (may be hidden) 'detached' — element has been removed from the DOM
Inputs: selector (required), state, timeout_ms. Returns: result
wait_for_predicateA
Wait until a JavaScript expression returns a truthy value.
The general-purpose wait, for when readiness is not "an element appeared": a list reached a length, a spinner class was removed, a global got populated, an animation finished.
wait_for_predicate("document.querySelectorAll('.row').length > 10")
wait_for_predicate("!document.querySelector('.spinner')")
wait_for_predicate("window.__APP_READY === true")Always prefer this to a fixed sleep — it returns the moment the condition
holds, instead of costing the full delay every time. If you only need to
wait after a click or a keypress, pass wait_for_predicate to that tool
instead and save the extra round-trip.
Args: js: JavaScript expression (or zero-argument function) re-evaluated in page context until it returns truthy. timeout_ms: Maximum time to wait (default 10 000). poll_ms: How often to re-evaluate, in milliseconds (default 100).
Inputs: js (required), poll_ms, timeout_ms. Returns: result
wait_for_urlA
Wait for the page URL to contain a given substring.
Use this after clicking a SPA navigation link where the URL changes client-side without a full page reload. Returns once the URL matches or the timeout expires.
Args: url_contains: Substring the URL must contain (e.g. '/dashboard', '?tab=posts'). timeout_ms: Maximum time to wait in milliseconds (default 10 000).
Inputs: url_contains (required), timeout_ms. Returns: result
workspace_new_slideA
Add a new slide in Google Slides (equivalent to Ctrl+M).
Drops the pointer-events lock first so the toolbar is clickable, clicks the 'New slide' button, and settles. Then call workspace_write(target='slides', placeholder=N) to fill the new slide.
Inputs: no named parameters. Returns: result
workspace_writeA
Type text into a Google Workspace editor (Docs or Slides) reliably.
Google Docs/Slides render on , so fill_field/click can't place
text and execCommand is deprecated/flaky. This tool dismisses blocking side
panels, focuses the editor, and types with real keystrokes (the events the
canvas editor actually listens for), then verifies.
Docs: types at the current cursor (auto-removes the Gemini overlay and focuses the hidden input iframe). Slides: pass placeholder=N to pick the text box (0 = first, usually the title). Removes the onboarding modal, double-clicks the placeholder to enter edit mode, then types. Use workspace_new_slide() to add slides.
Newlines in text are sent as real Enter presses.
Args: text: Text to type. target: 'auto' (detect from URL), or force 'docs' / 'slides'. verify: Read the text back to confirm it landed (Docs only, best-effort). placeholder: Slides only — which text placeholder to edit (0-based, in document order; 0 is typically the title).
Inputs: text (required), target, verify, placeholder. Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 24 tools
Tools largely target distinct actions (navigate, click, fill_field, hover, scroll, select_option), and the wait tools are separated by what they wait for (element vs predicate vs URL). Minor overlaps exist—submit_form can be achieved with click or press_keys, and login overlaps with fill_field+submit_form—but descriptions clearly delineate these cases.
All tool names use consistent snake_case with verb-first semantics (store_credential, fill_field, wait_for_url, get_page_info, close_browser). Single-word verbs like navigate/click/hover/scroll remain within the same predictable style, so the surface reads uniformly.
24 tools is on the heavier side for the rubric, but browser automation inherently spans navigation, interaction, waiting, credential management, browser lifecycle, and Workspace editing—each tool does appear to earn its place. No obvious redundant tools, so it lands slightly over rather than bloated.
The surface covers a full lifecycle: browser setup/open/close, navigation, interaction, multiple wait primitives, form handling, page reading, JS evaluation, and credential storage. Gaps like screenshots, tab/window management, and file upload/download exist but are workable around, especially via evaluate.
Maintenance
Related MCP Connectors
Host any HTTP API as a standard MCP server; auto-distribute to 12+ mainstream agent platforms.
License checks, signed rights proof, keyless pre-call checks for MCP servers, and the agent store
Agent communication platform for agent to agent messaging via MCP. Messages, channels, skills.
Use your Pronto feed, Spaces, Posts, Artifacts, and agents from any MCP client.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables MCP-capable agents to create and invoke OpenServerless endpoints, manage secrets, and configure integrations such as S3, PostgreSQL, Redis, Milvus, and MongoDB.13Apache 2.0
- AlicenseNot gradedqualityBmaintenanceEnables agent clients to safely connect to tools and execution resources through MCP with authorization, approvals, audit, chat-context isolation, SSH/Docker access, and long-running command session tracking.MIT
- AlicenseNot gradedqualityBmaintenanceProvides OAuth-based MCP integration and client-side tooling for connecting agents to a remote service, with CLI and plugin support.MIT
- AlicenseCqualityCmaintenanceEnables any MCP client to triage cloud agents, projects, conversations, transcripts, pull requests, and files, including listing agents, reading transcripts, following runs, and sending follow-ups, using an API key or browser sign-in.29MIT