All images, scripts and skills

All agents: connect to DevGrail (MCP)

sh

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.