diff --git a/modules/programs/vscode/default.nix b/modules/programs/vscode/default.nix index 4eba83784..5c89da6e5 100644 --- a/modules/programs/vscode/default.nix +++ b/modules/programs/vscode/default.nix @@ -114,6 +114,33 @@ let isPath = p: builtins.isPath p || lib.isStorePath p; + transformMcpServerForVscode = + name: server: + let + # Remove the disabled field from the server config + cleanServer = lib.filterAttrs (n: v: n != "disabled") server; + in + { + name = name; + value = { + enabled = !(server.disabled or false); + } + // ( + if server ? url then + { + type = "http"; + } + // cleanServer + else if server ? command then + { + type = "stdio"; + } + // cleanServer + else + { } + ); + }; + profileType = types.submodule { options = { userSettings = mkOption { @@ -154,6 +181,20 @@ let ''; }; + enableMcpIntegration = mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether to integrate the MCP servers config from + {option}`programs.mcp.servers` into + {option}`programs.vscode.profiles..userMcp`. + + Note: Settings defined in {option}`programs.mcp.servers` are merged + with {option}`programs.vscode.profiles..userMcp`, with VSCode + settings taking precedence. + ''; + }; + userMcp = mkOption { type = types.either types.path jsonFormat.type; default = { }; @@ -459,10 +500,31 @@ in if isPath v.userTasks then v.userTasks else jsonFormat.generate "vscode-user-tasks" v.userTasks; }) - (mkIf (v.userMcp != { }) { - "${mcpFilePath n}".source = - if isPath v.userMcp then v.userMcp else jsonFormat.generate "vscode-user-mcp" v.userMcp; - }) + (mkIf + ( + v.userMcp != { } + || (v.enableMcpIntegration && config.programs.mcp.enable && config.programs.mcp.servers != { }) + ) + { + "${mcpFilePath n}".source = + if isPath v.userMcp then + v.userMcp + else + let + transformedMcpServers = + if v.enableMcpIntegration && config.programs.mcp.enable && config.programs.mcp.servers != { } then + lib.listToAttrs (lib.mapAttrsToList transformMcpServerForVscode config.programs.mcp.servers) + else + { }; + # Merge MCP servers: transformed servers + user servers, with user servers taking precedence + mergedServers = transformedMcpServers // ((v.userMcp.servers or { })); + # Merge all MCP config + mergedMcpConfig = + v.userMcp // (lib.optionalAttrs (mergedServers != { }) { servers = mergedServers; }); + in + jsonFormat.generate "vscode-user-mcp" mergedMcpConfig; + } + ) (mkIf (v.keybindings != [ ]) { "${keybindingsFilePath n}".source = diff --git a/tests/modules/programs/vscode/default.nix b/tests/modules/programs/vscode/default.nix index 4db8b2c8a..a62521746 100644 --- a/tests/modules/programs/vscode/default.nix +++ b/tests/modules/programs/vscode/default.nix @@ -24,6 +24,8 @@ let keybindings = import ./keybindings.nix; tasks = import ./tasks.nix; mcp = import ./mcp.nix; + mcp-integration = import ./mcp-integration.nix; + mcp-integration-with-override = import ./mcp-integration-with-override.nix; update-checks = import ./update-checks.nix; snippets = import ./snippets.nix; }; diff --git a/tests/modules/programs/vscode/mcp-integration-default.json b/tests/modules/programs/vscode/mcp-integration-default.json new file mode 100644 index 000000000..66e3dfd42 --- /dev/null +++ b/tests/modules/programs/vscode/mcp-integration-default.json @@ -0,0 +1,26 @@ +{ + "servers": { + "context7": { + "enabled": true, + "headers": { + "CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}" + }, + "type": "http", + "url": "https://mcp.context7.com/mcp" + }, + "disabled-server": { + "command": "echo", + "enabled": false, + "type": "stdio" + }, + "everything": { + "args": [ + "-y", + "@modelcontextprotocol/server-everything" + ], + "command": "npx", + "enabled": true, + "type": "stdio" + } + } +} diff --git a/tests/modules/programs/vscode/mcp-integration-test.json b/tests/modules/programs/vscode/mcp-integration-test.json new file mode 100644 index 000000000..4f35aeaf1 --- /dev/null +++ b/tests/modules/programs/vscode/mcp-integration-test.json @@ -0,0 +1,7 @@ +{ + "servers": { + "Github": { + "url": "https://api.githubcopilot.com/mcp/" + } + } +} diff --git a/tests/modules/programs/vscode/mcp-integration-with-override.json b/tests/modules/programs/vscode/mcp-integration-with-override.json new file mode 100644 index 000000000..10772d0bf --- /dev/null +++ b/tests/modules/programs/vscode/mcp-integration-with-override.json @@ -0,0 +1,25 @@ +{ + "servers": { + "CustomServer": { + "type": "http", + "url": "https://example.com/mcp" + }, + "context7": { + "enabled": true, + "headers": { + "CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}" + }, + "type": "http", + "url": "https://mcp.context7.com/mcp" + }, + "everything": { + "args": [ + "-y", + "@modelcontextprotocol/server-everything" + ], + "command": "custom-npx", + "enabled": false, + "type": "stdio" + } + } +} diff --git a/tests/modules/programs/vscode/mcp-integration-with-override.nix b/tests/modules/programs/vscode/mcp-integration-with-override.nix new file mode 100644 index 000000000..a1d7371f0 --- /dev/null +++ b/tests/modules/programs/vscode/mcp-integration-with-override.nix @@ -0,0 +1,79 @@ +package: + +{ + config, + pkgs, + lib, + ... +}: + +let + cfg = config.programs.vscode; + willUseIfd = package.pname != "vscode"; + + mcpFilePath = + name: + if pkgs.stdenv.hostPlatform.isDarwin then + "Library/Application Support/${cfg.nameShort}/User/${ + lib.optionalString (name != "default") "profiles/${name}/" + }mcp.json" + else + ".config/${cfg.nameShort}/User/${ + lib.optionalString (name != "default") "profiles/${name}/" + }mcp.json"; + +in + +lib.mkIf (willUseIfd -> config.test.enableLegacyIfd) { + programs.mcp = { + enable = true; + servers = { + everything = { + command = "npx"; + args = [ + "-y" + "@modelcontextprotocol/server-everything" + ]; + }; + context7 = { + url = "https://mcp.context7.com/mcp"; + headers = { + CONTEXT7_API_KEY = "{env:CONTEXT7_API_KEY}"; + }; + }; + }; + }; + + programs.vscode = { + enable = true; + inherit package; + profiles = { + default = { + enableMcpIntegration = true; + # User MCP settings should override generated ones + userMcp = { + servers = { + everything = { + command = "custom-npx"; + args = [ + "-y" + "@modelcontextprotocol/server-everything" + ]; + enabled = false; + type = "stdio"; + }; + CustomServer = { + type = "http"; + url = "https://example.com/mcp"; + }; + }; + }; + }; + }; + }; + + nmt.script = '' + assertFileExists "home-files/${mcpFilePath "default"}" + assertFileContent "home-files/${mcpFilePath "default"}" ${./mcp-integration-with-override.json} + ''; +} diff --git a/tests/modules/programs/vscode/mcp-integration.nix b/tests/modules/programs/vscode/mcp-integration.nix new file mode 100644 index 000000000..e40056747 --- /dev/null +++ b/tests/modules/programs/vscode/mcp-integration.nix @@ -0,0 +1,73 @@ +package: + +{ + config, + pkgs, + lib, + ... +}: + +let + cfg = config.programs.vscode; + willUseIfd = package.pname != "vscode"; + + mcpFilePath = + name: + if pkgs.stdenv.hostPlatform.isDarwin then + "Library/Application Support/${cfg.nameShort}/User/${ + lib.optionalString (name != "default") "profiles/${name}/" + }mcp.json" + else + ".config/${cfg.nameShort}/User/${ + lib.optionalString (name != "default") "profiles/${name}/" + }mcp.json"; + +in + +lib.mkIf (willUseIfd -> config.test.enableLegacyIfd) { + programs.mcp = { + enable = true; + servers = { + everything = { + command = "npx"; + args = [ + "-y" + "@modelcontextprotocol/server-everything" + ]; + }; + context7 = { + url = "https://mcp.context7.com/mcp"; + headers = { + CONTEXT7_API_KEY = "{env:CONTEXT7_API_KEY}"; + }; + }; + disabled-server = { + command = "echo"; + disabled = true; + }; + }; + }; + + programs.vscode = { + enable = true; + inherit package; + profiles = { + default.enableMcpIntegration = true; + test.userMcp = { + servers = { + Github = { + url = "https://api.githubcopilot.com/mcp/"; + }; + }; + }; + }; + }; + + nmt.script = '' + assertFileExists "home-files/${mcpFilePath "default"}" + assertFileContent "home-files/${mcpFilePath "default"}" ${./mcp-integration-default.json} + + assertFileExists "home-files/${mcpFilePath "test"}" + assertFileContent "home-files/${mcpFilePath "test"}" ${./mcp-integration-test.json} + ''; +}