aboutsummaryrefslogtreecommitdiff
path: root/mcp/run.sh
diff options
context:
space:
mode:
Diffstat (limited to 'mcp/run.sh')
-rwxr-xr-xmcp/run.sh83
1 files changed, 83 insertions, 0 deletions
diff --git a/mcp/run.sh b/mcp/run.sh
new file mode 100755
index 0000000..27c8ad9
--- /dev/null
+++ b/mcp/run.sh
@@ -0,0 +1,83 @@
+#!/bin/sh
+
+# Starts the MCP server (server.mjs), installing its pinned dependencies first when they are
+# missing or the lockfile changed. Arguments go to server.mjs: none for stdio, which MCP clients
+# launch once per session, or --http HOST:PORT for a shared server. --install-only installs and
+# exits, which is worth doing once after cloning so the first client start is not a long one.
+#
+# stdout is the MCP protocol, so everything here writes to stderr.
+
+set -eu
+
+here="$(cd "$(dirname "$0")" && pwd)"
+
+# MCP clients start servers with a bare environment: Claude Desktop in particular has no node on
+# its PATH. Add the usual places one is installed, without disturbing an inherited PATH.
+PATH="${PATH}:${HOME}/.local/share/n/bin:${HOME}/.volta/bin:/opt/homebrew/bin:/usr/local/bin"
+export PATH
+
+# nvm installs per version, with no stable bin directory, so take the newest it has.
+if ! command -v node >/dev/null 2>&1; then
+ for dir in "${HOME}"/.nvm/versions/node/*/bin; do
+ [ -x "${dir}/node" ] && nvm_bin="${dir}"
+ done
+ [ -n "${nvm_bin:-}" ] && PATH="${nvm_bin}:${PATH}" && export PATH
+fi
+
+if ! command -v node >/dev/null 2>&1; then
+ echo "pi-chrome-mcp: no node on PATH; install Node 20.6 or newer, or start this with one on PATH" >&2
+ exit 1
+fi
+
+stamp="${here}/node_modules/.pi-chrome-mcp-deps"
+if ! cmp -s "${here}/package-lock.json" "${stamp}" 2>/dev/null; then
+ # Two clients starting at once would otherwise run npm ci over each other.
+ lock="${here}/.install-lock"
+ tries=0
+ until mkdir "${lock}" 2>/dev/null; do
+ tries=$((tries + 1))
+ if [ "${tries}" -gt 120 ]; then
+ echo "pi-chrome-mcp: gave up waiting for ${lock}; remove it if no install is running" >&2
+ exit 1
+ fi
+ sleep 1
+ done
+ trap 'rmdir "${lock}" 2>/dev/null || true' EXIT
+ if ! cmp -s "${here}/package-lock.json" "${stamp}" 2>/dev/null; then
+ echo "pi-chrome-mcp: installing pinned dependencies" >&2
+ npm_config_cache="${here}/.npm-cache" \
+ npm ci --prefix "${here}" --no-audit --no-fund --silent >&2
+ cp "${here}/package-lock.json" "${stamp}"
+ fi
+ rmdir "${lock}" 2>/dev/null || true
+ trap - EXIT
+fi
+
+# Chrome loads the companion extension from ./extension rather than from node_modules, where it
+# ships: npm ci deletes that whole tree when dependencies change, and an unpacked extension whose
+# folder disappears is disabled until someone loads it again by hand. This copy is refreshed only
+# when pi-chrome's version of it differs, so a reinstall that changes nothing leaves Chrome alone.
+# Compared by content, not by the manifest's version: two releases could ship the same version of
+# the extension with different files, and a copy that silently stayed behind would be debugged as a
+# server bug.
+packaged="${here}/node_modules/pi-chrome/extensions/chrome-profile-bridge/browser-extension"
+if [ -d "${packaged}" ]; then
+ digest="$(cd "${packaged}" && find . -type f -print | sort | xargs cat | cksum)"
+ # Kept beside the folder rather than in it: Chrome loads that folder, and nothing there should
+ # be anything but the extension.
+ stamp_ext="${here}/.extension-source"
+ if [ "${digest}" != "$(cat "${stamp_ext}" 2>/dev/null || true)" ]; then
+ echo "pi-chrome-mcp: updating ./extension; reload it at chrome://extensions" >&2
+ rm -rf "${here}/extension.new"
+ cp -R "${packaged}" "${here}/extension.new"
+ rm -rf "${here}/extension"
+ mv "${here}/extension.new" "${here}/extension"
+ echo "${digest}" > "${stamp_ext}"
+ fi
+fi
+
+if [ "${1:-}" = "--install-only" ]; then
+ echo "pi-chrome-mcp: ready. Load this folder in chrome://extensions: ${here}/extension" >&2
+ exit 0
+fi
+exec node "${here}/server.mjs" "$@"