Skip to content
Open
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
43 changes: 43 additions & 0 deletions registry/coder/modules/vscode-desktop-core/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import {
runTerraformApply,
runTerraformInit,
testRequiredVariables,
runContainer,
execContainer,
removeContainer,
findResourceInstance,
readFileContainer,
} from "~test";

// hardcoded coder_app name in main.tf
Expand All @@ -16,6 +21,7 @@ const defaultVariables = {
coder_app_display_name: "VS Code Desktop",

protocol: "vscode",
config_folder: "$HOME/.vscode",
};

describe("vscode-desktop-core", async () => {
Expand Down Expand Up @@ -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);
});
50 changes: 38 additions & 12 deletions registry/coder/modules/vscode-desktop-core/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe config_dir is better

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. Reads better

type = string
description = "The path of the IDE's configuration folder."
}

variable "coder_app_icon" {
type = string
description = "The icon of the coder_app."
Expand Down Expand Up @@ -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."
}
}