feat(coder/modules/claude-code): add support for MCP server configurations from remote URLs (#668)
## Description
- add support for MCP server configurations from remote URLs
## Example
```json
mcp_remote_urls = [
"https://gist.githubusercontent.com/35C4n0r/cd8dce70360e5d22a070ae21893caed4/raw/",
"https://raw.githubusercontent.com/coder/coder/main/.mcp.json"
]
```
## Type of Change
- [ ] New module
- [ ] New template
- [ ] Bug fix
- [x] Feature/enhancement
- [ ] Documentation
- [ ] Other
## Module Information
<!-- Delete this section if not applicable -->
**Path:** `registry/coder/modules/claude-code`
**New version:** `v4.6.0`
**Breaking change:** [ ] Yes [x] No
## Testing & Validation
- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun fmt`)
- [x] Changes tested locally
## Related Issues
Closes: #665
This commit is contained in:
parent
01365fb61a
commit
01d6669708
@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
|
|||||||
```tf
|
```tf
|
||||||
module "claude-code" {
|
module "claude-code" {
|
||||||
source = "registry.coder.com/coder/claude-code/coder"
|
source = "registry.coder.com/coder/claude-code/coder"
|
||||||
version = "4.5.0"
|
version = "4.6.0"
|
||||||
agent_id = coder_agent.main.id
|
agent_id = coder_agent.main.id
|
||||||
workdir = "/home/coder/project"
|
workdir = "/home/coder/project"
|
||||||
claude_api_key = "xxxx-xxxxx-xxxx"
|
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
|
```tf
|
||||||
module "claude-code" {
|
module "claude-code" {
|
||||||
source = "registry.coder.com/coder/claude-code/coder"
|
source = "registry.coder.com/coder/claude-code/coder"
|
||||||
version = "4.5.0"
|
version = "4.6.0"
|
||||||
agent_id = coder_agent.main.id
|
agent_id = coder_agent.main.id
|
||||||
workdir = "/home/coder/project"
|
workdir = "/home/coder/project"
|
||||||
enable_boundary = true
|
enable_boundary = true
|
||||||
@ -64,7 +64,7 @@ For tasks integration with AI Bridge, add `enable_aibridge = true` to the [Usage
|
|||||||
```tf
|
```tf
|
||||||
module "claude-code" {
|
module "claude-code" {
|
||||||
source = "registry.coder.com/coder/claude-code/coder"
|
source = "registry.coder.com/coder/claude-code/coder"
|
||||||
version = "4.5.0"
|
version = "4.6.0"
|
||||||
agent_id = coder_agent.main.id
|
agent_id = coder_agent.main.id
|
||||||
workdir = "/home/coder/project"
|
workdir = "/home/coder/project"
|
||||||
enable_aibridge = true
|
enable_aibridge = true
|
||||||
@ -93,7 +93,7 @@ data "coder_task" "me" {}
|
|||||||
|
|
||||||
module "claude-code" {
|
module "claude-code" {
|
||||||
source = "registry.coder.com/coder/claude-code/coder"
|
source = "registry.coder.com/coder/claude-code/coder"
|
||||||
version = "4.5.0"
|
version = "4.6.0"
|
||||||
agent_id = coder_agent.main.id
|
agent_id = coder_agent.main.id
|
||||||
workdir = "/home/coder/project"
|
workdir = "/home/coder/project"
|
||||||
claude_api_key = "xxxx-xxxxx-xxxx"
|
claude_api_key = "xxxx-xxxxx-xxxx"
|
||||||
@ -114,7 +114,7 @@ This example shows additional configuration options for version pinning, custom
|
|||||||
```tf
|
```tf
|
||||||
module "claude-code" {
|
module "claude-code" {
|
||||||
source = "registry.coder.com/coder/claude-code/coder"
|
source = "registry.coder.com/coder/claude-code/coder"
|
||||||
version = "4.5.0"
|
version = "4.6.0"
|
||||||
agent_id = coder_agent.main.id
|
agent_id = coder_agent.main.id
|
||||||
workdir = "/home/coder/project"
|
workdir = "/home/coder/project"
|
||||||
|
|
||||||
@ -139,9 +139,30 @@ module "claude-code" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
EOF
|
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
|
### Standalone Mode
|
||||||
|
|
||||||
Run and configure Claude Code as a standalone CLI in your workspace.
|
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
|
```tf
|
||||||
module "claude-code" {
|
module "claude-code" {
|
||||||
source = "registry.coder.com/coder/claude-code/coder"
|
source = "registry.coder.com/coder/claude-code/coder"
|
||||||
version = "4.5.0"
|
version = "4.6.0"
|
||||||
agent_id = coder_agent.main.id
|
agent_id = coder_agent.main.id
|
||||||
workdir = "/home/coder/project"
|
workdir = "/home/coder/project"
|
||||||
install_claude_code = true
|
install_claude_code = true
|
||||||
@ -171,7 +192,7 @@ variable "claude_code_oauth_token" {
|
|||||||
|
|
||||||
module "claude-code" {
|
module "claude-code" {
|
||||||
source = "registry.coder.com/coder/claude-code/coder"
|
source = "registry.coder.com/coder/claude-code/coder"
|
||||||
version = "4.5.0"
|
version = "4.6.0"
|
||||||
agent_id = coder_agent.main.id
|
agent_id = coder_agent.main.id
|
||||||
workdir = "/home/coder/project"
|
workdir = "/home/coder/project"
|
||||||
claude_code_oauth_token = var.claude_code_oauth_token
|
claude_code_oauth_token = var.claude_code_oauth_token
|
||||||
@ -244,7 +265,7 @@ resource "coder_env" "bedrock_api_key" {
|
|||||||
|
|
||||||
module "claude-code" {
|
module "claude-code" {
|
||||||
source = "registry.coder.com/coder/claude-code/coder"
|
source = "registry.coder.com/coder/claude-code/coder"
|
||||||
version = "4.5.0"
|
version = "4.6.0"
|
||||||
agent_id = coder_agent.main.id
|
agent_id = coder_agent.main.id
|
||||||
workdir = "/home/coder/project"
|
workdir = "/home/coder/project"
|
||||||
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||||
@ -301,7 +322,7 @@ resource "coder_env" "google_application_credentials" {
|
|||||||
|
|
||||||
module "claude-code" {
|
module "claude-code" {
|
||||||
source = "registry.coder.com/coder/claude-code/coder"
|
source = "registry.coder.com/coder/claude-code/coder"
|
||||||
version = "4.5.0"
|
version = "4.6.0"
|
||||||
agent_id = coder_agent.main.id
|
agent_id = coder_agent.main.id
|
||||||
workdir = "/home/coder/project"
|
workdir = "/home/coder/project"
|
||||||
model = "claude-sonnet-4@20250514"
|
model = "claude-sonnet-4@20250514"
|
||||||
|
|||||||
@ -461,4 +461,54 @@ EOF`,
|
|||||||
expect(startLog.stdout).toContain(taskSessionId);
|
expect(startLog.stdout).toContain(taskSessionId);
|
||||||
expect(startLog.stdout).not.toContain("manual-456");
|
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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -166,6 +166,12 @@ variable "mcp" {
|
|||||||
default = ""
|
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" {
|
variable "allowed_tools" {
|
||||||
type = string
|
type = string
|
||||||
description = "A list of tools that should be allowed without prompting the user for permission, in addition to settings.json files."
|
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_ALLOWED_TOOLS='${var.allowed_tools}' \
|
||||||
ARG_DISALLOWED_TOOLS='${var.disallowed_tools}' \
|
ARG_DISALLOWED_TOOLS='${var.disallowed_tools}' \
|
||||||
ARG_MCP='${var.mcp != null ? base64encode(replace(var.mcp, "'", "'\\''")) : ""}' \
|
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}' \
|
ARG_ENABLE_AIBRIDGE='${var.enable_aibridge}' \
|
||||||
/tmp/install.sh
|
/tmp/install.sh
|
||||||
EOT
|
EOT
|
||||||
|
|||||||
@ -16,6 +16,7 @@ ARG_INSTALL_VIA_NPM=${ARG_INSTALL_VIA_NPM:-false}
|
|||||||
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
|
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
|
||||||
ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-}
|
ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-}
|
||||||
ARG_MCP=$(echo -n "${ARG_MCP:-}" | base64 -d)
|
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_ALLOWED_TOOLS=${ARG_ALLOWED_TOOLS:-}
|
||||||
ARG_DISALLOWED_TOOLS=${ARG_DISALLOWED_TOOLS:-}
|
ARG_DISALLOWED_TOOLS=${ARG_DISALLOWED_TOOLS:-}
|
||||||
ARG_ENABLE_AIBRIDGE=${ARG_ENABLE_AIBRIDGE:-false}
|
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_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS"
|
||||||
printf "ARG_MCP_APP_STATUS_SLUG: %s\n" "$ARG_MCP_APP_STATUS_SLUG"
|
printf "ARG_MCP_APP_STATUS_SLUG: %s\n" "$ARG_MCP_APP_STATUS_SLUG"
|
||||||
printf "ARG_MCP: %s\n" "$ARG_MCP"
|
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_ALLOWED_TOOLS: %s\n" "$ARG_ALLOWED_TOOLS"
|
||||||
printf "ARG_DISALLOWED_TOOLS: %s\n" "$ARG_DISALLOWED_TOOLS"
|
printf "ARG_DISALLOWED_TOOLS: %s\n" "$ARG_DISALLOWED_TOOLS"
|
||||||
printf "ARG_ENABLE_AIBRIDGE: %s\n" "$ARG_ENABLE_AIBRIDGE"
|
printf "ARG_ENABLE_AIBRIDGE: %s\n" "$ARG_ENABLE_AIBRIDGE"
|
||||||
|
|
||||||
echo "--------------------------------"
|
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() {
|
function ensure_claude_in_path() {
|
||||||
if [ -z "${CODER_SCRIPT_BIN_DIR:-}" ]; then
|
if [ -z "${CODER_SCRIPT_BIN_DIR:-}" ]; then
|
||||||
echo "CODER_SCRIPT_BIN_DIR not set, skipping PATH setup"
|
echo "CODER_SCRIPT_BIN_DIR not set, skipping PATH setup"
|
||||||
@ -112,13 +127,25 @@ function setup_claude_configurations() {
|
|||||||
if [ "$ARG_MCP" != "" ]; then
|
if [ "$ARG_MCP" != "" ]; then
|
||||||
(
|
(
|
||||||
cd "$ARG_WORKDIR"
|
cd "$ARG_WORKDIR"
|
||||||
while IFS= read -r server_name && IFS= read -r server_json; do
|
add_mcp_servers "$ARG_MCP" "in $ARG_WORKDIR"
|
||||||
echo "------------------------"
|
)
|
||||||
echo "Executing: claude mcp add-json \"$server_name\" '$server_json' (in $ARG_WORKDIR)"
|
fi
|
||||||
claude mcp add-json "$server_name" "$server_json" || echo "Warning: Failed to add MCP server '$server_name', continuing..."
|
|
||||||
echo "------------------------"
|
if [ -n "$ARG_MCP_CONFIG_REMOTE_PATH" ] && [ "$ARG_MCP_CONFIG_REMOTE_PATH" != "[]" ]; then
|
||||||
echo ""
|
(
|
||||||
done < <(echo "$ARG_MCP" | jq -r '.mcpServers | to_entries[] | .key, (.value | @json)')
|
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
|
fi
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user