All agents: connect to DevGrail (MCP)
Registers the `devgrail` MCP server with every agent installed in this workspace — Claude Code (`~/.claude.json`), Codex (`config.toml` + spliced auth header), Antigravity (`~/.gemini/config/mcp_config.json`) and OpenCode (`opencode.json`, jq-merged) — in one run, from one key and URL. Agents not on PATH are skipped, not failed. Each is configured independently, so one failure doesn't stop the others, and the run ends with a summary of what was configured, left unchanged, not found and failed. Exits non-zero if a present agent fails or no agent CLI was found. Existing registrations are left untouched unless **Replace existing registration** is `yes` — use that after rotating the key or changing the URL. Configs are backed up; Codex rolls back on failure. **Agents to configure** can target one agent. MCP only, no logins — run each agent's own credentials script for headless sign-in. **Security:** the key is stored in cleartext as a bearer header in each config.
What it asks for
- DevGrail API keyrequired
- The key every configured agent will send to the DevGrail MCP server. Its grants decide what the agents can see and do — create a scoped key rather than reusing an admin one.
- MCP URL (override)
- Leave empty on current workspace images — the URL comes from the container's DEVGRAIL_MCP_URL. Older images don't set it; then enter it here, e.g. https://your-devgrail-host/mcp
- Replace existing registration
- Choose 'yes' to remove an existing 'devgrail' entry in each agent and re-add it — needed after rotating the API key or changing the URL. With 'no' existing entries are left untouched.
- Agents to configure
- Which agents to wire up. 'all' configures every one of the four that is installed in this container; pick a single agent to limit the run to it.
What it runs
The whole script, exactly as it will execute in your workspace. Nothing is hidden — read it before you run it.
#!/usr/bin/env sh
# Register the DevGrail MCP server with every coding agent installed in this
# workspace: Claude Code, Codex, Antigravity and OpenCode.
#
# Agents that are not on PATH are reported and skipped, not treated as errors —
# a container normally has only one or two of them.
set -eu
umask 077
PATH="$HOME/.local/bin:${HOME:-/home/dev}/.local/bin:$PATH"
export PATH
log() { printf '[devgrail-mcp] %s\n' "$*"; }
warn() { printf '[devgrail-mcp] WARN: %s\n' "$*" >&2; }
die() { printf '[devgrail-mcp] ERROR: %s\n' "$*" >&2; exit 1; }
MCP_NAME="devgrail"
API_KEY="${DEVGRAIL_API_KEY:-}"
# Workspace images export DEVGRAIL_MCP_URL; the param lets older images override.
MCP_URL="${DEVGRAIL_MCP_URL_OVERRIDE:-${DEVGRAIL_MCP_URL:-}}"
REPLACE="${REPLACE_EXISTING:-no}"
TARGET="${TARGET_AGENTS:-all}"
[ -n "$API_KEY" ] || die "no DevGrail API key was supplied"
[ -n "$MCP_URL" ] || die "DEVGRAIL_MCP_URL is not set in this container (older workspace images do not provide it) — fill in the 'MCP URL (override)' parameter, e.g. https://your-devgrail-host/mcp"
case "$TARGET" in
all|claude|codex|antigravity|opencode) ;;
*) die "unknown value for 'Agents to configure': $TARGET" ;;
esac
CONFIGURED=""
SKIPPED=""
MISSING=""
FAILED=""
wanted() { [ "$TARGET" = "all" ] || [ "$TARGET" = "$1" ]; }
# ---------------------------------------------------------------- Claude Code
configure_claude() {
if claude mcp get "$MCP_NAME" >/dev/null 2>&1; then
if [ "$REPLACE" != "yes" ]; then
log "claude: '$MCP_NAME' is already registered — left untouched"
SKIPPED="$SKIPPED claude"
return 0
fi
claude mcp remove --scope user "$MCP_NAME" >/dev/null 2>&1 \
|| claude mcp remove "$MCP_NAME" >/dev/null 2>&1 \
|| { warn "claude: could not remove the existing '$MCP_NAME' entry"; return 1; }
log "claude: removed the existing entry so it can be re-added"
fi
# This body is piped to 'sh -s' on stdin, so stdin IS the rest of the
# script: keep the CLI from consuming it.
claude mcp add --transport http --scope user "$MCP_NAME" "$MCP_URL" \
--header "Authorization: Bearer $API_KEY" </dev/null >/dev/null \
|| { warn "claude: 'claude mcp add' failed"; return 1; }
claude mcp get "$MCP_NAME" >/dev/null 2>&1 \
|| { warn "claude: entry was added but cannot be read back — check ~/.claude.json"; return 1; }
log "claude: registered '$MCP_NAME' (user scope, ~/.claude.json)"
CONFIGURED="$CONFIGURED claude"
return 0
}
# ---------------------------------------------------------------------- Codex
configure_codex() {
CODEX_DIR="${CODEX_HOME:-$HOME/.codex}"
CONFIG_FILE="$CODEX_DIR/config.toml"
BACKUP_FILE="$CONFIG_FILE.bak"
HAD_CONFIG="no"
EXISTING="no"
# A static TOML string needs a single-line token; DevGrail keys use this
# conservative URL-safe character set.
case "$API_KEY" in
*[!A-Za-z0-9._~-]*)
warn "codex: the API key contains characters that cannot be stored safely in config.toml"
return 1 ;;
esac
mkdir -p "$CODEX_DIR"
if codex mcp get "$MCP_NAME" >/dev/null 2>&1; then
EXISTING="yes"
if [ "$REPLACE" != "yes" ]; then
log "codex: '$MCP_NAME' is already registered — left untouched"
SKIPPED="$SKIPPED codex"
return 0
fi
fi
if [ -f "$CONFIG_FILE" ]; then
HAD_CONFIG="yes"
cp -p "$CONFIG_FILE" "$BACKUP_FILE"
log "codex: backed up existing config to $BACKUP_FILE"
fi
restore_codex_config() {
if [ "$HAD_CONFIG" = "yes" ]; then
cp -p "$BACKUP_FILE" "$CONFIG_FILE"
else
rm -f "$CONFIG_FILE"
fi
}
if [ "$EXISTING" = "yes" ]; then
codex mcp remove "$MCP_NAME" >/dev/null 2>&1 \
|| { restore_codex_config; warn "codex: could not remove the existing '$MCP_NAME' entry"; return 1; }
log "codex: removed the existing entry so it can be re-added"
fi
codex mcp add "$MCP_NAME" --url "$MCP_URL" </dev/null >/dev/null \
|| { restore_codex_config; warn "codex: 'codex mcp add' failed"; return 1; }
# 'codex mcp add' has no flag for auth headers, so splice the header into
# the [mcp_servers.devgrail] table by hand.
HEADER_LINE="http_headers = { Authorization = \"Bearer $API_KEY\" }"
tmp="$CONFIG_FILE.devgrail.tmp"
if awk -v header="$HEADER_LINE" '
BEGIN { in_target = 0; inserted = 0 }
$0 == "[mcp_servers.devgrail]" {
in_target = 1
print
next
}
/^\[/ {
if (in_target && !inserted) {
print header
inserted = 1
}
in_target = 0
}
{
if (in_target && $0 ~ /^[[:space:]]*http_headers[[:space:]]*=/) {
if (!inserted) {
print header
inserted = 1
}
next
}
print
}
END {
if (in_target && !inserted) {
print header
inserted = 1
}
if (!inserted) exit 42
}
' "$CONFIG_FILE" > "$tmp"; then
mv "$tmp" "$CONFIG_FILE"
else
rm -f "$tmp"
restore_codex_config
warn "codex: could not add the authorization header to $CONFIG_FILE"
return 1
fi
chmod 600 "$CONFIG_FILE"
codex mcp get "$MCP_NAME" >/dev/null 2>&1 \
|| { restore_codex_config; warn "codex: entry was added but cannot be read back — restored the previous config"; return 1; }
log "codex: registered '$MCP_NAME' ($CONFIG_FILE)"
CONFIGURED="$CONFIGURED codex"
return 0
}
# ---------------------------------------------------------------- Antigravity
antigravity_server_exists() {
agy mcp list 2>/dev/null | awk 'NR>1 {print $1}' | grep -Fxq "$MCP_NAME" \
|| { [ -f "$HOME/.gemini/config/mcp_config.json" ] && command -v jq >/dev/null 2>&1 \
&& jq -e --arg n "$MCP_NAME" '.mcpServers[$n] // empty' "$HOME/.gemini/config/mcp_config.json" >/dev/null 2>&1; }
}
configure_antigravity() {
if antigravity_server_exists; then
if [ "$REPLACE" != "yes" ]; then
log "antigravity: '$MCP_NAME' is already registered — left untouched"
SKIPPED="$SKIPPED antigravity"
return 0
fi
agy mcp remove "$MCP_NAME" </dev/null >/dev/null 2>&1 || true
log "antigravity: removed the existing entry so it can be re-added"
fi
# This body is piped to 'sh -s' on stdin, so stdin IS the rest of the
# script: keep the CLI from consuming it.
agy mcp add --header "Authorization: Bearer $API_KEY" "$MCP_NAME" "$MCP_URL" </dev/null >/dev/null 2>&1 \
|| { warn "antigravity: 'agy mcp add' failed"; return 1; }
antigravity_server_exists \
|| { warn "antigravity: entry was added but cannot be read back — check ~/.gemini/config/mcp_config.json"; return 1; }
log "antigravity: registered '$MCP_NAME' (~/.gemini/config/mcp_config.json)"
CONFIGURED="$CONFIGURED antigravity"
return 0
}
# ------------------------------------------------------------------- OpenCode
configure_opencode() {
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/opencode"
CONFIG_FILE="$CONFIG_DIR/opencode.json"
mkdir -p "$CONFIG_DIR"
if [ -s "$CONFIG_FILE" ] && command -v jq >/dev/null 2>&1 \
&& jq -e '.mcp.devgrail // empty' "$CONFIG_FILE" >/dev/null 2>&1; then
if [ "$REPLACE" != "yes" ]; then
log "opencode: '$MCP_NAME' is already registered — left untouched"
SKIPPED="$SKIPPED opencode"
return 0
fi
fi
# "oauth": false is deliberate. DevGrail answers an unauthenticated /mcp
# with a WWW-Authenticate challenge, which an MCP client reads as an
# invitation to start an OAuth flow. This server is authenticated by the
# bearer header below, so a bad key should surface as a plain 401.
SERVER="$(cat <<EOF
{
"type": "remote",
"url": "$MCP_URL",
"enabled": true,
"oauth": false,
"headers": {
"Authorization": "Bearer $API_KEY"
}
}
EOF
)"
if [ -f "$CONFIG_FILE" ]; then
cp -p "$CONFIG_FILE" "$CONFIG_FILE.bak"
log "opencode: backed up existing config to $CONFIG_FILE.bak"
fi
if command -v jq >/dev/null 2>&1; then
[ -s "$CONFIG_FILE" ] || printf '%s\n' '{}' > "$CONFIG_FILE"
tmp="$CONFIG_FILE.devgrail.tmp"
if jq --argjson srv "$SERVER" \
'.["$schema"] //= "https://opencode.ai/config.json"
| .mcp //= {}
| .mcp.devgrail = $srv' \
"$CONFIG_FILE" > "$tmp"; then
mv "$tmp" "$CONFIG_FILE"
else
rm -f "$tmp"
warn "opencode: could not merge into $CONFIG_FILE — left unchanged"
return 1
fi
elif [ -s "$CONFIG_FILE" ]; then
warn "opencode: jq is not installed and $CONFIG_FILE already has content — refusing to overwrite it; install jq (sudo apt-get install -y jq) and re-run"
return 1
else
cat > "$CONFIG_FILE" <<EOF
{
"\$schema": "https://opencode.ai/config.json",
"mcp": {
"devgrail": $SERVER
}
}
EOF
fi
chmod 600 "$CONFIG_FILE"
log "opencode: registered '$MCP_NAME' ($CONFIG_FILE)"
CONFIGURED="$CONFIGURED opencode"
return 0
}
# ----------------------------------------------------------------------- main
for agent in claude codex antigravity opencode; do
wanted "$agent" || continue
cli_cmd="$agent"
if [ "$agent" = "antigravity" ]; then
if command -v agy >/dev/null 2>&1; then
cli_cmd="agy"
elif command -v antigravity >/dev/null 2>&1; then
cli_cmd="antigravity"
fi
fi
if ! command -v "$cli_cmd" >/dev/null 2>&1; then
log "$agent: CLI not on PATH — skipping"
MISSING="$MISSING $agent"
continue
fi
case "$agent" in
claude) configure_claude || FAILED="$FAILED claude" ;;
codex) configure_codex || FAILED="$FAILED codex" ;;
antigravity) configure_antigravity || FAILED="$FAILED antigravity" ;;
opencode) configure_opencode || FAILED="$FAILED opencode" ;;
esac
done
log "----"
log "MCP URL: $MCP_URL"
log "configured:${CONFIGURED:- none}"
log "unchanged: ${SKIPPED:- none}"
log "not found: ${MISSING:- none}"
log "failed: ${FAILED:- none}"
if [ -n "$FAILED" ]; then
die "one or more agents could not be configured:${FAILED}"
fi
if [ -z "$CONFIGURED" ] && [ -z "$SKIPPED" ]; then
die "no supported agent CLI was found in this container — nothing was configured"
fi
if [ -n "$SKIPPED" ]; then
log "To point an unchanged agent at a new key or URL, re-run with 'Replace existing registration' = yes."
fi
log "Security note: the bearer token is stored in cleartext in each agent's config."
log "Next: start one of the agents and ask it to list your DevGrail containers."
How to get it
Open your DevGrail workspace, go to Scripts → Official scripts, and choose Download. It is copied into your own library, where you can edit it, run it, or set it as a workspace default — the copy is yours, and later changes here do not touch it.