#!/bin/sh # Browz MCP server: one-line install for macOS and Linux (KAN-12034). # # curl -fsSL https://gobrowz.com/agents/install.sh | sh # put browz-mcp on your PATH # curl -fsSL https://gobrowz.com/agents/install.sh | sh -s -- claude-desktop # ...and add it to a client # # The MCP server ships inside the Browz app, so nothing is downloaded: this finds the browz-mcp binary # bundled with Browz, links it into ~/.local/bin (or $BROWZ_BIN_DIR), and, when you name a client, # adds Browz to that client's configuration. A client's config file is merged, never replaced: other # servers and settings are kept, the previous file is saved as .browz-backup, and running it again # changes nothing. No token is written anywhere: the bundled server reads the app's own credentials. # # Clients it configures: claude-code, claude-desktop, cursor, gemini-cli. For the others it prints the # page with their setup. Windows: use the app's Automate > Connect an AI agent screen instead. # # Environment: BROWZ_MCP= skips the search; BROWZ_BIN_DIR= changes where the # link goes. BROWZ_INSTALL_OS, BROWZ_INSTALL_ROOT and BROWZ_INSTALL_JSON exist for the tests only # (sdk/mcp/test/install.test.mjs). # # Everything is inside main(), called on the last line, so a download cut short runs nothing. main() { set -eu DOCS=https://gobrowz.com/docs/ai-agents.html DOWNLOAD=https://gobrowz.com/download.html CLIENT= for arg in "$@"; do case "$arg" in -h|--help) usage; return 0 ;; -*) err "unknown option $arg"; usage >&2; return 2 ;; *) [ -z "$CLIENT" ] || { err "name one client"; return 2; }; CLIENT=$arg ;; esac done OS=${BROWZ_INSTALL_OS:-$(uname -s)} ROOT=${BROWZ_INSTALL_ROOT:-} BIN_DIR=${BROWZ_BIN_DIR:-$HOME/.local/bin} LINK=$BIN_DIR/browz-mcp case "$OS" in Darwin|Linux) ;; *) err "this installer is for macOS and Linux. On Windows, open Browz and use Automate > Connect an AI agent, or see $DOCS"; return 1 ;; esac BIN=$(find_binary) || { if [ -n "${BROWZ_MCP:-}" ]; then err "BROWZ_MCP is set to $BROWZ_MCP, which is not an executable file." else err "the Browz app was not found. Install Browz from $DOWNLOAD and open it (no account needed for local use), then run this again." fi return 3 } say "Found the Browz MCP server: $BIN" link_binary case "$CLIENT" in '') say "Next: add Browz to your AI client, for example:" say " curl -fsSL https://gobrowz.com/agents/install.sh | sh -s -- claude-desktop" say "Every client's setup: $DOCS" ;; claude-code) setup_claude_code ;; claude-desktop|cursor|gemini-cli) setup_json "$CLIENT" ;; claude-ai|openai|grok|vscode|browser-use|stagehand|langchain) say "$CLIENT is set up in its own settings or code; the steps are at $DOCS#$CLIENT" ;; *) err "unknown client $CLIENT (one of: claude-code, claude-desktop, cursor, gemini-cli, claude-ai, openai, grok, vscode, browser-use, stagehand, langchain)"; return 2 ;; esac say "Browz has to be open while your agent uses it." } usage() { cat <<'EOF' Usage: curl -fsSL https://gobrowz.com/agents/install.sh | sh -s -- [client] Links the browz-mcp server bundled with Browz into ~/.local/bin and, if you name a client, adds Browz to it. Clients: claude-code, claude-desktop, cursor, gemini-cli (others: see https://gobrowz.com/docs/ai-agents.html). EOF } say() { printf '%s\n' "$*"; } err() { printf 'browz install: %s\n' "$*" >&2; } # Same order as sdk/mcp/lib/locate.mjs: BROWZ_MCP alone, else the app's install places, else PATH. # Our own link is skipped so a stale link is never mistaken for the app. find_binary() { if [ -n "${BROWZ_MCP:-}" ]; then [ -f "$BROWZ_MCP" ] && [ -x "$BROWZ_MCP" ] && { printf '%s\n' "$BROWZ_MCP"; return 0; } return 1 fi if [ "$OS" = Darwin ]; then set -- "$ROOT/Applications/Browz.app/Contents/Resources/browz-mcp" "$HOME/Applications/Browz.app/Contents/Resources/browz-mcp" else set -- "$HOME/.local/lib/browz/browz-mcp" "$HOME/.local/bin/browz-mcp" "$ROOT/usr/lib/browz/browz-mcp" "$ROOT/opt/browz/browz-mcp" fi for c in "$@"; do [ "$c" = "$LINK" ] && [ -L "$c" ] && continue [ -f "$c" ] && [ -x "$c" ] && { printf '%s\n' "$c"; return 0; } done c=$(command -v browz-mcp 2>/dev/null || true) if [ -n "$c" ] && [ "$c" != "$LINK" ] && [ -x "$c" ]; then printf '%s\n' "$c"; return 0; fi return 1 } link_binary() { if [ "$BIN" = "$LINK" ]; then say "browz-mcp is already in $BIN_DIR." elif [ -e "$LINK" ] && [ ! -L "$LINK" ]; then say "Left $LINK alone (it is not a link this installer made)." else mkdir -p "$BIN_DIR" ln -sf "$BIN" "$LINK" say "Linked $LINK -> $BIN" fi case ":${PATH:-}:" in *":$BIN_DIR:"*) ;; *) say "Note: $BIN_DIR is not on your PATH; add it to run browz-mcp by name (client configs use the full path)." ;; esac } setup_claude_code() { if ! command -v claude >/dev/null 2>&1; then say "Claude Code (claude) is not on your PATH. Once it is, run:" say " claude mcp add --scope user --transport stdio browz -- '$BIN'" return 0 fi if claude mcp get browz >/dev/null 2>&1; then say "Claude Code already has a server called browz; nothing changed (claude mcp remove browz to replace it)." return 0 fi claude mcp add --scope user --transport stdio browz -- "$BIN" say "Added Browz to Claude Code. Run /mcp in Claude Code to check it." } config_file() { case "$1" in claude-desktop) if [ "$OS" = Darwin ]; then printf '%s\n' "$HOME/Library/Application Support/Claude/claude_desktop_config.json" else printf '%s\n' "${XDG_CONFIG_HOME:-$HOME/.config}/Claude/claude_desktop_config.json"; fi ;; cursor) printf '%s\n' "$HOME/.cursor/mcp.json" ;; gemini-cli) printf '%s\n' "$HOME/.gemini/settings.json" ;; esac } # The merge, in Node or Python (whichever is here), with the rules of sdk/mcp/lib/setup.mjs mergeConfig. # Prints "new" (file created), "backup" (file changed, old copy kept) or "same", or fails without # touching the file. Inputs come in the environment. MERGE_JS='const fs=require("fs");const f=process.env.BZ_FILE,e={command:process.env.BZ_BIN,args:[]}; let d={};if(fs.existsSync(f)){const t=fs.readFileSync(f,"utf8");if(t.trim()){d=JSON.parse(t); if(!d||typeof d!=="object"||Array.isArray(d))throw new Error("not a JSON object")}} const s=d.mcpServers&&typeof d.mcpServers==="object"&&!Array.isArray(d.mcpServers)?d.mcpServers:{}; if(JSON.stringify(s.browz)===JSON.stringify(e)){console.log("same");process.exit(0)} d.mcpServers={...s,browz:e};fs.mkdirSync(require("path").dirname(f),{recursive:true}); const b=fs.existsSync(f);if(b)fs.copyFileSync(f,f+".browz-backup"); fs.writeFileSync(f,JSON.stringify(d,null,2)+"\n");console.log(b?"backup":"new")' MERGE_PY='import json,os,shutil,sys f=os.environ["BZ_FILE"];e={"command":os.environ["BZ_BIN"],"args":[]};d={} if os.path.exists(f): t=open(f,encoding="utf-8").read() if t.strip(): d=json.loads(t) if not isinstance(d,dict): sys.exit("not a JSON object") s=d.get("mcpServers");s=s if isinstance(s,dict) else {} if s.get("browz")==e: print("same"); sys.exit(0) s["browz"]=e;d["mcpServers"]=s;os.makedirs(os.path.dirname(f),exist_ok=True) b=os.path.exists(f) if b: shutil.copyfile(f,f+".browz-backup") open(f,"w",encoding="utf-8").write(json.dumps(d,indent=2,ensure_ascii=False)+"\n");print("backup" if b else "new")' json_tool() { want=${BROWZ_INSTALL_JSON:-} if [ "$want" != python ] && command -v node >/dev/null 2>&1; then echo node; return 0; fi if [ "$want" != node ] && command -v python3 >/dev/null 2>&1; then # macOS's /usr/bin/python3 is a stub that pops up an installer when the developer tools are missing. if [ "$OS" != Darwin ] || [ "$(command -v python3)" != /usr/bin/python3 ] || xcode-select -p >/dev/null 2>&1; then echo python; return 0 fi fi return 1 } setup_json() { file=$(config_file "$1") if ! tool=$(json_tool); then say "Neither Node.js nor Python is here to edit $file safely. Add this under \"mcpServers\" in it yourself:" say " \"browz\": { \"command\": \"$BIN\", \"args\": [] }" return 0 fi if [ "$tool" = node ]; then result=$(BZ_FILE=$file BZ_BIN=$BIN node -e "$MERGE_JS" 2>&1) || { err "could not update $file: $result"; return 1; } else result=$(BZ_FILE=$file BZ_BIN=$BIN python3 -c "$MERGE_PY" 2>&1) || { err "could not update $file: $result"; return 1; } fi case "$result" in *same) say "$file already has this Browz entry; nothing changed." ;; *backup) say "Added Browz to $file (previous file kept as $file.browz-backup). Restart $1 to load it." ;; *new) say "Added Browz to $file. Restart $1 to load it." ;; *) err "unexpected result from the config merge: $result"; return 1 ;; esac } main "$@"