diff --git a/registry/coder/modules/vscode-desktop-core/main.test.ts b/registry/coder/modules/vscode-desktop-core/main.test.ts index 87674f32d..2db12a2d0 100644 --- a/registry/coder/modules/vscode-desktop-core/main.test.ts +++ b/registry/coder/modules/vscode-desktop-core/main.test.ts @@ -3,6 +3,11 @@ import { runTerraformApply, runTerraformInit, testRequiredVariables, + runContainer, + execContainer, + removeContainer, + findResourceInstance, + readFileContainer, } from "~test"; // hardcoded coder_app name in main.tf @@ -16,6 +21,7 @@ const defaultVariables = { coder_app_display_name: "VS Code Desktop", protocol: "vscode", + config_folder: "$HOME/.vscode", }; describe("vscode-desktop-core", async () => { @@ -134,4 +140,41 @@ describe("vscode-desktop-core", async () => { expect(coder_app?.instances[0].attributes.group).toBe("web-app-group"); }); }); + + it("writes mcp_config.json when mcp_config variable provided", async () => { + const id = await runContainer("alpine"); + + try { + const mcp_config = JSON.stringify({ + servers: { demo: { url: "http://localhost:1234" } }, + }); + + const state = await runTerraformApply(import.meta.dir, { + ...defaultVariables, + + mcp_config, + }); + + const script = findResourceInstance( + state, + "coder_script", + "vscode-desktop-mcp", + ).script; + + const resp = await execContainer(id, ["sh", "-c", script]); + if (resp.exitCode !== 0) { + console.log(resp.stdout); + console.log(resp.stderr); + } + expect(resp.exitCode).toBe(0); + + const content = await readFileContainer( + id, + `${defaultVariables.config_folder.replace("$HOME", "/root")}/mcp_config.json`, + ); + expect(content).toBe(mcp_config); + } finally { + await removeContainer(id); + } + }, 10000); }); diff --git a/registry/coder/modules/vscode-desktop-core/main.tf b/registry/coder/modules/vscode-desktop-core/main.tf index 9a7da34c1..9ea962886 100644 --- a/registry/coder/modules/vscode-desktop-core/main.tf +++ b/registry/coder/modules/vscode-desktop-core/main.tf @@ -26,11 +26,22 @@ variable "open_recent" { default = false } +variable "mcp_config" { + type = map(any) + description = "MCP server configuration for the IDE. When set, writes mcp_config.json in var.config_folder." + default = null +} + variable "protocol" { type = string description = "The URI protocol the IDE." } +variable "config_folder" { + type = string + description = "The path of the IDE's configuration folder." +} + variable "coder_app_icon" { type = string description = "The icon of the coder_app." @@ -85,21 +96,36 @@ resource "coder_app" "vscode-desktop" { data.coder_workspace.me.access_url, "&token=$SESSION_TOKEN", ]) +} - /* - url = join("", [ - "vscode://coder.coder-remote/open", - "?owner=${data.coder_workspace_owner.me.name}", - "&workspace=${data.coder_workspace.me.name}", - var.folder != "" ? join("", ["&folder=", var.folder]) : "", - var.open_recent ? "&openRecent" : "", - "&url=${data.coder_workspace.me.access_url}", - "&token=$SESSION_TOKEN", - ]) - */ +resource "coder_script" "vscode-desktop-mcp" { + agent_id = var.agent_id + count = var.mcp_config != null ? 1 : 0 + + icon = var.coder_app_icon + display_name = "${var.coder_app_display_name} MCP" + + run_on_start = true + start_blocks_login = false + + script = <<-EOT + #!/bin/sh + set -euo pipefail + + IDE_CONFIG_FOLDER="${var.config_folder}" + IDE_MCP_CONFIG_PATH="$IDE_CONFIG_FOLDER/mcp_config.json" + + mkdir -p "$IDE_CONFIG_FOLDER" + + echo -n "${base64encode(jsonencode(var.mcp_config))}" | base64 -d > "$IDE_MCP_CONFIG_PATH" + chmod 600 "$IDE_MCP_CONFIG_PATH" + + # Cursor/Windsurf use this config instead, no need for chmod as symlinks do not have modes + ln -s "$IDE_MCP_CONFIG_PATH" "$IDE_CONFIG_FOLDER/mcp.json" + EOT } output "ide_uri" { value = coder_app.vscode-desktop.url description = "IDE URI." -} \ No newline at end of file +}