diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index eff7dfdd1..8e98a880a 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.5.0" + version = "4.6.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -45,7 +45,7 @@ This example shows how to configure the Claude Code module to run the agent behi ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.5.0" + version = "4.6.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_boundary = true @@ -64,7 +64,7 @@ For tasks integration with AI Bridge, add `enable_aibridge = true` to the [Usage ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.5.0" + version = "4.6.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_aibridge = true @@ -93,7 +93,7 @@ data "coder_task" "me" {} module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.5.0" + version = "4.6.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -114,7 +114,7 @@ This example shows additional configuration options for version pinning, custom ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.5.0" + version = "4.6.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" @@ -139,9 +139,30 @@ module "claude-code" { } } EOF + + mcp_config_remote_path = [ + "https://gist.githubusercontent.com/35C4n0r/cd8dce70360e5d22a070ae21893caed4/raw/", + "https://raw.githubusercontent.com/coder/coder/main/.mcp.json" + ] } ``` +> [!NOTE] +> Remote URLs should return a JSON body in the following format: +> +> ```json +> { +> "mcpServers": { +> "server-name": { +> "command": "some-command", +> "args": ["arg1", "arg2"] +> } +> } +> } +> ``` +> +> The `Content-Type` header doesn't matter—both `text/plain` and `application/json` work fine. + ### Standalone Mode Run and configure Claude Code as a standalone CLI in your workspace. @@ -149,7 +170,7 @@ Run and configure Claude Code as a standalone CLI in your workspace. ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.5.0" + version = "4.6.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" install_claude_code = true @@ -171,7 +192,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.5.0" + version = "4.6.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -244,7 +265,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.5.0" + version = "4.6.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -301,7 +322,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.5.0" + version = "4.6.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" diff --git a/registry/coder/modules/claude-code/main.test.ts b/registry/coder/modules/claude-code/main.test.ts index d59b6a8f4..19ab98c04 100644 --- a/registry/coder/modules/claude-code/main.test.ts +++ b/registry/coder/modules/claude-code/main.test.ts @@ -461,4 +461,54 @@ EOF`, expect(startLog.stdout).toContain(taskSessionId); expect(startLog.stdout).not.toContain("manual-456"); }); + + test("mcp-config-remote-path", async () => { + const failingUrl = "http://localhost:19999/mcp.json"; + const successUrl = + "https://raw.githubusercontent.com/coder/coder/main/.mcp.json"; + + const { id, coderEnvVars } = await setup({ + skipClaudeMock: true, + moduleVariables: { + mcp_config_remote_path: JSON.stringify([failingUrl, successUrl]), + }, + }); + await execModuleScript(id, coderEnvVars); + + const installLog = await readFileContainer( + id, + "/home/coder/.claude-module/install.log", + ); + + // Verify both URLs are attempted + expect(installLog).toContain(failingUrl); + expect(installLog).toContain(successUrl); + + // First URL should fail gracefully + expect(installLog).toContain( + `Warning: Failed to fetch MCP configuration from '${failingUrl}'`, + ); + + // Second URL should succeed - no failure warning for it + expect(installLog).not.toContain( + `Warning: Failed to fetch MCP configuration from '${successUrl}'`, + ); + + // Should contain the MCP server add command from successful fetch + expect(installLog).toContain( + "Added stdio MCP server go-language-server to local config", + ); + + expect(installLog).toContain( + "Added stdio MCP server typescript-language-server to local config", + ); + + // Verify the MCP config was added to claude.json + const claudeConfig = await readFileContainer( + id, + "/home/coder/.claude.json", + ); + expect(claudeConfig).toContain("typescript-language-server"); + expect(claudeConfig).toContain("go-language-server"); + }); }); diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index dd5402790..3a1128b48 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -166,6 +166,12 @@ variable "mcp" { default = "" } +variable "mcp_config_remote_path" { + type = list(string) + description = "List of URLs that return JSON MCP server configurations (text/plain with valid JSON)" + default = [] +} + variable "allowed_tools" { type = string description = "A list of tools that should be allowed without prompting the user for permission, in addition to settings.json files." @@ -404,6 +410,7 @@ module "agentapi" { ARG_ALLOWED_TOOLS='${var.allowed_tools}' \ ARG_DISALLOWED_TOOLS='${var.disallowed_tools}' \ ARG_MCP='${var.mcp != null ? base64encode(replace(var.mcp, "'", "'\\''")) : ""}' \ + ARG_MCP_CONFIG_REMOTE_PATH='${base64encode(jsonencode(var.mcp_config_remote_path))}' \ ARG_ENABLE_AIBRIDGE='${var.enable_aibridge}' \ /tmp/install.sh EOT diff --git a/registry/coder/modules/claude-code/scripts/install.sh b/registry/coder/modules/claude-code/scripts/install.sh index 07e199c18..b8e4fba2b 100644 --- a/registry/coder/modules/claude-code/scripts/install.sh +++ b/registry/coder/modules/claude-code/scripts/install.sh @@ -16,6 +16,7 @@ ARG_INSTALL_VIA_NPM=${ARG_INSTALL_VIA_NPM:-false} ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true} ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-} ARG_MCP=$(echo -n "${ARG_MCP:-}" | base64 -d) +ARG_MCP_CONFIG_REMOTE_PATH=$(echo -n "${ARG_MCP_CONFIG_REMOTE_PATH:-}" | base64 -d) ARG_ALLOWED_TOOLS=${ARG_ALLOWED_TOOLS:-} ARG_DISALLOWED_TOOLS=${ARG_DISALLOWED_TOOLS:-} ARG_ENABLE_AIBRIDGE=${ARG_ENABLE_AIBRIDGE:-false} @@ -30,12 +31,26 @@ printf "ARG_INSTALL_VIA_NPM: %s\n" "$ARG_INSTALL_VIA_NPM" printf "ARG_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS" printf "ARG_MCP_APP_STATUS_SLUG: %s\n" "$ARG_MCP_APP_STATUS_SLUG" printf "ARG_MCP: %s\n" "$ARG_MCP" +printf "ARG_MCP_CONFIG_REMOTE_PATH: %s\n" "$ARG_MCP_CONFIG_REMOTE_PATH" printf "ARG_ALLOWED_TOOLS: %s\n" "$ARG_ALLOWED_TOOLS" printf "ARG_DISALLOWED_TOOLS: %s\n" "$ARG_DISALLOWED_TOOLS" printf "ARG_ENABLE_AIBRIDGE: %s\n" "$ARG_ENABLE_AIBRIDGE" echo "--------------------------------" +function add_mcp_servers() { + local mcp_json="$1" + local source_desc="$2" + + while IFS= read -r server_name && IFS= read -r server_json; do + echo "------------------------" + echo "Executing: claude mcp add-json \"$server_name\" '$server_json' ($source_desc)" + claude mcp add-json "$server_name" "$server_json" || echo "Warning: Failed to add MCP server '$server_name', continuing..." + echo "------------------------" + echo "" + done < <(echo "$mcp_json" | jq -r '.mcpServers | to_entries[] | .key, (.value | @json)') +} + function ensure_claude_in_path() { if [ -z "${CODER_SCRIPT_BIN_DIR:-}" ]; then echo "CODER_SCRIPT_BIN_DIR not set, skipping PATH setup" @@ -112,13 +127,25 @@ function setup_claude_configurations() { if [ "$ARG_MCP" != "" ]; then ( cd "$ARG_WORKDIR" - while IFS= read -r server_name && IFS= read -r server_json; do - echo "------------------------" - echo "Executing: claude mcp add-json \"$server_name\" '$server_json' (in $ARG_WORKDIR)" - claude mcp add-json "$server_name" "$server_json" || echo "Warning: Failed to add MCP server '$server_name', continuing..." - echo "------------------------" - echo "" - done < <(echo "$ARG_MCP" | jq -r '.mcpServers | to_entries[] | .key, (.value | @json)') + add_mcp_servers "$ARG_MCP" "in $ARG_WORKDIR" + ) + fi + + if [ -n "$ARG_MCP_CONFIG_REMOTE_PATH" ] && [ "$ARG_MCP_CONFIG_REMOTE_PATH" != "[]" ]; then + ( + cd "$ARG_WORKDIR" + for url in $(echo "$ARG_MCP_CONFIG_REMOTE_PATH" | jq -r '.[]'); do + echo "Fetching MCP configuration from: $url" + mcp_json=$(curl -fsSL "$url") || { + echo "Warning: Failed to fetch MCP configuration from '$url', continuing..." + continue + } + if ! echo "$mcp_json" | jq -e '.mcpServers' > /dev/null 2>&1; then + echo "Warning: Invalid MCP configuration from '$url' (missing mcpServers), continuing..." + continue + fi + add_mcp_servers "$mcp_json" "from $url" + done ) fi