---
mode: subagent
---

<!-- SPDX-License-Identifier: MIT -->
<!-- SPDX-FileCopyrightText: 2025-2026 Marcus Quinn -->
# t1665.2: Create mcp-config-adapter.sh — Universal MCP Config Transform

## Origin

- **Created:** 2026-03-26
- **Session:** claude-code:interactive
- **Created by:** marcusquinn (human + ai-interactive)
- **Parent task:** t1665
- **Conversation context:** Audit found 3 completely different MCP registration mechanisms. The MCP server definitions (command, args, env) are identical across runtimes — only the config format differs.

## What

A new shell library `.agents/scripts/mcp-config-adapter.sh` that takes a universal MCP server definition and writes it to any runtime's config in the correct format. Includes command validation (skip entries where the binary doesn't exist).

## Why

- 3 different MCP registration mechanisms exist: JSON file edit (OpenCode, Cursor), CLI command (Claude, Droid), TOML file edit (Codex)
- Each has different field names (`env` vs `environment`), structure (`command` as string vs array), and root keys (`mcp` vs `mcpServers` vs `mcp_servers`)
- The MCP _content_ (server name, command, args, env vars) is identical — only delivery format differs
- The Codex Docker MCP failure is caused by writing an MCP entry without validating the command exists
- `ai-cli-config.sh` already demonstrates the right pattern (per-runtime functions) but is limited to one MCP server

## How (Approach)

### File: `.agents/scripts/mcp-config-adapter.sh`

Sources `runtime-registry.sh` for runtime properties.

**Universal MCP definition format** (JSON, matches existing `configs/mcp-templates/` pattern):

```json
{
  "name": "my-mcp",
  "command": "npx",
  "args": ["-y", "@example/mcp@latest"],
  "env": {"API_KEY": "your-key"},
  "transport": "stdio",
  "enabled": true
}
```

**Core function:**

```bash
register_mcp_for_runtime() {
    local runtime_id="$1"    # e.g., "opencode", "claude", "codex"
    local mcp_name="$2"      # e.g., "playwright"
    local mcp_json="$3"      # Universal JSON definition

    # 1. Validate command exists
    local cmd
    cmd=$(echo "$mcp_json" | jq -r '.command')
    if ! command -v "$cmd" &>/dev/null; then
        print_warning "Skipping $mcp_name for $(get_runtime_display_name "$runtime_id"): '$cmd' not found"
        return 0
    fi

    # 2. Get runtime format and dispatch to adapter
    local format
    format=$(get_runtime_config_format "$runtime_id")
    case "$format" in
        json-opencode)      _register_mcp_opencode "$mcp_name" "$mcp_json" ;;
        cli-claude)         _register_mcp_claude "$mcp_name" "$mcp_json" ;;
        toml-codex)         _register_mcp_codex "$mcp_name" "$mcp_json" ;;
        json-mcpServers)    _register_mcp_mcpservers "$runtime_id" "$mcp_name" "$mcp_json" ;;
        cli-droid)          _register_mcp_droid "$mcp_name" "$mcp_json" ;;
        json-array-continue) _register_mcp_continue "$mcp_name" "$mcp_json" ;;
        yaml-aider)         _register_mcp_aider "$mcp_name" "$mcp_json" ;;
        *) print_warning "Unknown config format '$format' for runtime $runtime_id" ;;
    esac
}
```

**Per-format adapter functions:**

```bash
_register_mcp_opencode() {
    # Transform: command string + args array → single command array
    # Rename: env → environment
    # Add: type: "local", enabled: true
    # Write to: ~/.config/opencode/opencode.json under .mcp key
}

_register_mcp_claude() {
    # Build JSON: {type: "stdio", command: "...", args: [...], env: {...}}
    # Execute: claude mcp add-json NAME --scope user 'JSON'
}

_register_mcp_codex() {
    # Emit TOML: [mcp_servers.NAME] section
    # command = 'string', args = ['array']
    # Append to ~/.codex/config.toml (or create if missing)
}

_register_mcp_mcpservers() {
    # Generic JSON: {mcpServers: {NAME: {command, args, env}}}
    # Used by: Cursor, Windsurf, Gemini, Kilo, Kiro
    # Config path from registry
}

_register_mcp_droid() {
    # Execute: droid mcp add NAME COMMAND ARGS --env KEY=VALUE
}

_register_mcp_continue() {
    # Append to mcpServers array: {name, transport: {type, command, args}}
}
```

**Batch registration:**

```bash
register_mcp_for_all_runtimes() {
    local mcp_name="$1"
    local mcp_json="$2"
    local runtime_id
    while IFS= read -r runtime_id; do
        register_mcp_for_runtime "$runtime_id" "$mcp_name" "$mcp_json"
    done < <(detect_installed_runtimes)
}
```

**TOML writer** (for Codex — no `toml` CLI assumed):

```bash
_toml_write_mcp_section() {
    local config_file="$1"
    local mcp_name="$2"
    local command="$3"
    local args_json="$4"  # JSON array string

    # Check if section already exists
    if grep -q "^\[mcp_servers\.$mcp_name\]" "$config_file" 2>/dev/null; then
        print_info "$mcp_name already configured in $config_file"
        return 0
    fi

    # Ensure [mcp_servers] header exists
    if ! grep -q '^\[mcp_servers\]' "$config_file" 2>/dev/null; then
        printf '\n[mcp_servers]\n' >> "$config_file"
    fi

    # Append section
    printf '\n[mcp_servers.%s]\n' "$mcp_name" >> "$config_file"
    printf "command = '%s'\n" "$command" >> "$config_file"

    # Convert JSON args array to TOML array
    local toml_args
    toml_args=$(echo "$args_json" | jq -r '[.[] | "'"'"'" + . + "'"'"'"] | join(", ")')
    printf 'args = [%s]\n' "$toml_args" >> "$config_file"
}
```

### Key files to reference

- `.agents/scripts/ai-cli-config.sh:22-113` — existing `json_set_nested()` and `json_append_to_array()` helpers (reuse these)
- `.agents/scripts/ai-cli-config.sh:124-286` — existing per-runtime MCP config functions (pattern to follow)
- `configs/mcp-templates/augment-context-engine.json` — existing multi-runtime template format
- `.agents/scripts/generate-opencode-agents.sh` — current OpenCode MCP registration
- `.agents/scripts/generate-claude-agents.sh` — current Claude MCP registration
- `.agents/scripts/mcp-register-claude.sh` — current Claude-only MCP registration (484 lines)

## Acceptance Criteria

- [ ] `mcp-config-adapter.sh` exists at `.agents/scripts/mcp-config-adapter.sh`
- [ ] `register_mcp_for_runtime "opencode" "test" '{"command":"echo","args":["hello"]}'` writes correct entry to opencode.json
- [ ] `register_mcp_for_runtime "codex" "test" '{"command":"echo","args":["hello"]}'` writes correct TOML to config.toml
- [ ] `register_mcp_for_runtime "cursor" "test" '{"command":"echo","args":["hello"]}'` writes correct entry to mcp.json
- [ ] `register_mcp_for_runtime "claude" "test" '{"command":"echo","args":["hello"]}'` calls `claude mcp add-json` correctly (or skips if claude not installed)
- [ ] Command validation: `register_mcp_for_runtime "codex" "docker-mcp" '{"command":"docker","args":["mcp","gateway","run"]}'` skips with warning when `docker mcp` is not available
- [ ] `register_mcp_for_all_runtimes` iterates all installed runtimes
- [ ] Idempotent: running twice doesn't create duplicate entries
- [ ] Bash 3.2 compatible, shellcheck clean
- [ ] Reuses `json_set_nested()` from `ai-cli-config.sh` (or equivalent)
