Skip to content

Commit ca61878

Browse files
committed
mcp: init module
1 parent bbaeb9f commit ca61878

18 files changed

+599
-12
lines changed

modules/programs/mcp.nix

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{
2+
config,
3+
lib,
4+
pkgs,
5+
...
6+
}:
7+
let
8+
inherit (lib)
9+
literalExpression
10+
mkEnableOption
11+
mkIf
12+
mkOption
13+
;
14+
15+
cfg = config.programs.mcp;
16+
17+
jsonFormat = pkgs.formats.json { };
18+
in
19+
{
20+
meta.maintainers = with lib.maintainers; [ delafthi ];
21+
22+
options.programs.mcp = {
23+
enable = mkEnableOption "mcp";
24+
25+
servers = mkOption {
26+
inherit (jsonFormat) type;
27+
default = { };
28+
example = literalExpression ''
29+
{
30+
everything = {
31+
command = "npx";
32+
args = [
33+
"-y"
34+
"@modelcontextprotocol/server-everything"
35+
];
36+
};
37+
context7 = {
38+
url = "https://mcp.context7.com/mcp";
39+
headers = {
40+
CONTEXT7_API_KEY = "{env:CONTEXT7_API_KEY}";
41+
};
42+
};
43+
}
44+
'';
45+
description = ''
46+
MCP server configurations written to
47+
{file}`XDG_CONFIG_HOME/.config/mcp/mcp.json`
48+
'';
49+
};
50+
};
51+
52+
config = mkIf cfg.enable {
53+
xdg.configFile = mkIf (cfg.servers != { }) (
54+
let
55+
mcp-config = {
56+
mcpServers = cfg.servers;
57+
};
58+
in
59+
{
60+
"mcp/mcp.json".source = jsonFormat.generate "mcp.json" mcp-config;
61+
}
62+
);
63+
};
64+
}

modules/programs/opencode.nix

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,35 @@ let
1616
cfg = config.programs.opencode;
1717

1818
jsonFormat = pkgs.formats.json { };
19+
20+
transformMcpServer = name: server: {
21+
name = name;
22+
value = {
23+
enabled = !(server.disabled or false);
24+
}
25+
// (
26+
if server ? url then
27+
{
28+
type = "remote";
29+
url = server.url;
30+
}
31+
// (lib.optionalAttrs (server ? headers) { headers = server.headers; })
32+
else if server ? command then
33+
{
34+
type = "local";
35+
command = [ server.command ] ++ (server.args or [ ]);
36+
}
37+
// (lib.optionalAttrs (server ? env) { environment = server.env; })
38+
else
39+
{ }
40+
);
41+
};
42+
43+
transformedMcpServers =
44+
if cfg.enableMcpIntegration && config.programs.mcp.enable && config.programs.mcp.servers != { } then
45+
lib.listToAttrs (lib.mapAttrsToList transformMcpServer config.programs.mcp.servers)
46+
else
47+
{ };
1948
in
2049
{
2150
meta.maintainers = with lib.maintainers; [ delafthi ];
@@ -25,6 +54,20 @@ in
2554

2655
package = mkPackageOption pkgs "opencode" { nullable = true; };
2756

57+
enableMcpIntegration = mkOption {
58+
type = lib.types.bool;
59+
default = false;
60+
description = ''
61+
Whether to integrate the MCP servers config from
62+
{option}`programs.mcp.servers` into
63+
{option}`programs.opencode.settings.mcp`.
64+
65+
Note: Settings defined in {option}`programs.mcp.servers` are merged
66+
with {option}`programs.opencode.settings.mcp`, with OpenCode settings
67+
taking precedence.
68+
'';
69+
};
70+
2871
settings = mkOption {
2972
inherit (jsonFormat) type;
3073
default = { };
@@ -147,7 +190,7 @@ in
147190
Custom themes for opencode. The attribute name becomes the theme
148191
filename, and the value is either:
149192
- An attribute set, that is converted to a json
150-
- A path to a file conaining the content
193+
- A path to a file containing the content
151194
Themes are stored in {file}`$XDG_CONFIG_HOME/opencode/themes/` directory.
152195
Set `programs.opencode.settings.theme` to enable the custom theme.
153196
See <https://opencode.ai/docs/themes/> for the documentation.
@@ -159,13 +202,21 @@ in
159202
home.packages = mkIf (cfg.package != null) [ cfg.package ];
160203

161204
xdg.configFile = {
162-
"opencode/config.json" = mkIf (cfg.settings != { }) {
163-
source = jsonFormat.generate "config.json" (
164-
{
165-
"$schema" = "https://opencode.ai/config.json";
166-
}
167-
// cfg.settings
168-
);
205+
"opencode/config.json" = mkIf (cfg.settings != { } || transformedMcpServers != { }) {
206+
source =
207+
let
208+
# Merge MCP servers: transformed servers + user settings, with user settings taking precedence
209+
mergedMcpServers = transformedMcpServers // (cfg.settings.mcp or { });
210+
# Merge all settings
211+
mergedSettings =
212+
cfg.settings // (lib.optionalAttrs (mergedMcpServers != { }) { mcp = mergedMcpServers; });
213+
in
214+
jsonFormat.generate "config.json" (
215+
{
216+
"$schema" = "https://opencode.ai/config.json";
217+
}
218+
// mergedSettings
219+
);
169220
};
170221

171222
"opencode/AGENTS.md" = (

modules/programs/vscode/default.nix

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,33 @@ let
114114

115115
isPath = p: builtins.isPath p || lib.isStorePath p;
116116

117+
transformMcpServerForVscode =
118+
name: server:
119+
let
120+
# Remove the disabled field from the server config
121+
cleanServer = lib.filterAttrs (n: v: n != "disabled") server;
122+
in
123+
{
124+
name = name;
125+
value = {
126+
enabled = !(server.disabled or false);
127+
}
128+
// (
129+
if server ? url then
130+
{
131+
type = "http";
132+
}
133+
// cleanServer
134+
else if server ? command then
135+
{
136+
type = "stdio";
137+
}
138+
// cleanServer
139+
else
140+
{ }
141+
);
142+
};
143+
117144
profileType = types.submodule {
118145
options = {
119146
userSettings = mkOption {
@@ -154,6 +181,20 @@ let
154181
'';
155182
};
156183

184+
enableMcpIntegration = mkOption {
185+
type = lib.types.bool;
186+
default = false;
187+
description = ''
188+
Whether to integrate the MCP servers config from
189+
{option}`programs.mcp.servers` into
190+
{option}`programs.vscode.profiles.<name>.userMcp`.
191+
192+
Note: Settings defined in {option}`programs.mcp.servers` are merged
193+
with {option}`programs.vscode.profiles.<name>.userMcp`, with VSCode
194+
settings taking precedence.
195+
'';
196+
};
197+
157198
userMcp = mkOption {
158199
type = types.either types.path jsonFormat.type;
159200
default = { };
@@ -459,10 +500,31 @@ in
459500
if isPath v.userTasks then v.userTasks else jsonFormat.generate "vscode-user-tasks" v.userTasks;
460501
})
461502

462-
(mkIf (v.userMcp != { }) {
463-
"${mcpFilePath n}".source =
464-
if isPath v.userMcp then v.userMcp else jsonFormat.generate "vscode-user-mcp" v.userMcp;
465-
})
503+
(mkIf
504+
(
505+
v.userMcp != { }
506+
|| (v.enableMcpIntegration && config.programs.mcp.enable && config.programs.mcp.servers != { })
507+
)
508+
{
509+
"${mcpFilePath n}".source =
510+
if isPath v.userMcp then
511+
v.userMcp
512+
else
513+
let
514+
transformedMcpServers =
515+
if v.enableMcpIntegration && config.programs.mcp.enable && config.programs.mcp.servers != { } then
516+
lib.listToAttrs (lib.mapAttrsToList transformMcpServerForVscode config.programs.mcp.servers)
517+
else
518+
{ };
519+
# Merge MCP servers: transformed servers + user servers, with user servers taking precedence
520+
mergedServers = transformedMcpServers // ((v.userMcp.servers or { }));
521+
# Merge all MCP config
522+
mergedMcpConfig =
523+
v.userMcp // (lib.optionalAttrs (mergedServers != { }) { servers = mergedServers; });
524+
in
525+
jsonFormat.generate "vscode-user-mcp" mergedMcpConfig;
526+
}
527+
)
466528

467529
(mkIf (v.keybindings != [ ]) {
468530
"${keybindingsFilePath n}".source =
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
mcp-servers = ./servers.nix;
3+
mcp-empty-servers = ./empty-servers.nix;
4+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
programs.mcp = {
3+
enable = true;
4+
servers = { };
5+
};
6+
nmt.script = ''
7+
assertPathNotExists home-files/.config/mcp/mcp.json
8+
'';
9+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"mcpServers": {
3+
"context7": {
4+
"headers": {
5+
"CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}"
6+
},
7+
"serverUrl": "https://mcp.context7.com/mcp"
8+
},
9+
"everything": {
10+
"args": [
11+
"-y",
12+
"@modelcontextprotocol/server-everything"
13+
],
14+
"command": "npx"
15+
}
16+
}
17+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
programs.mcp = {
3+
enable = true;
4+
servers = {
5+
everything = {
6+
command = "npx";
7+
args = [
8+
"-y"
9+
"@modelcontextprotocol/server-everything"
10+
];
11+
};
12+
context7 = {
13+
serverUrl = "https://mcp.context7.com/mcp";
14+
headers = {
15+
CONTEXT7_API_KEY = "{env:CONTEXT7_API_KEY}";
16+
};
17+
};
18+
};
19+
};
20+
nmt.script = ''
21+
assertFileExists home-files/.config/mcp/mcp.json
22+
assertFileContent home-files/.config/mcp/mcp.json \
23+
${./mcp.json}
24+
'';
25+
}

tests/modules/programs/opencode/default.nix

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@
1111
opencode-mixed-content = ./mixed-content.nix;
1212
opencode-themes-inline = ./themes-inline.nix;
1313
opencode-themes-path = ./themes-path.nix;
14+
opencode-mcp-integration = ./mcp-integration.nix;
15+
opencode-mcp-integration-with-override = ./mcp-integration-with-override.nix;
1416
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"$schema": "https://opencode.ai/config.json",
3+
"mcp": {
4+
"context7": {
5+
"enabled": true,
6+
"headers": {
7+
"CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}"
8+
},
9+
"type": "remote",
10+
"url": "https://mcp.context7.com/mcp"
11+
},
12+
"custom-server": {
13+
"enabled": true,
14+
"type": "remote",
15+
"url": "https://example.com"
16+
},
17+
"everything": {
18+
"command": [
19+
"custom-command"
20+
],
21+
"enabled": false,
22+
"type": "local"
23+
}
24+
},
25+
"model": "anthropic/claude-sonnet-4-20250514",
26+
"theme": "opencode"
27+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
programs.mcp = {
3+
enable = true;
4+
servers = {
5+
everything = {
6+
command = "npx";
7+
args = [
8+
"-y"
9+
"@modelcontextprotocol/server-everything"
10+
];
11+
};
12+
context7 = {
13+
url = "https://mcp.context7.com/mcp";
14+
headers = {
15+
CONTEXT7_API_KEY = "{env:CONTEXT7_API_KEY}";
16+
};
17+
};
18+
};
19+
};
20+
21+
programs.opencode = {
22+
enable = true;
23+
enableMcpIntegration = true;
24+
settings = {
25+
theme = "opencode";
26+
model = "anthropic/claude-sonnet-4-20250514";
27+
# User's custom MCP settings should override generated ones
28+
mcp = {
29+
everything = {
30+
enabled = false; # Override to disable
31+
command = [ "custom-command" ];
32+
type = "local";
33+
};
34+
custom-server = {
35+
enabled = true;
36+
type = "remote";
37+
url = "https://example.com";
38+
};
39+
};
40+
};
41+
};
42+
43+
nmt.script = ''
44+
assertFileExists home-files/.config/opencode/config.json
45+
assertFileContent home-files/.config/opencode/config.json \
46+
${./mcp-integration-with-override.json}
47+
'';
48+
}

0 commit comments

Comments
 (0)