Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 30 additions & 9 deletions registry/coder/modules/claude-code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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"
Expand All @@ -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"

Expand All @@ -139,17 +139,38 @@ 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.

```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
Expand All @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
50 changes: 50 additions & 0 deletions registry/coder/modules/claude-code/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
7 changes: 7 additions & 0 deletions registry/coder/modules/claude-code/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down Expand Up @@ -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
Expand Down
41 changes: 34 additions & 7 deletions registry/coder/modules/claude-code/scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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"
Expand Down Expand Up @@ -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

Expand Down