claude-code: load MCP config via plugin dir

Claude Code rejects `--mcp-config` once the Home Manager wrapper injects
it around subcommands, which breaks commands like `claude mcp list`.
Claude Code 2.1.76 fixed `--plugin-dir` so it no longer consumes
following subcommands, so use that path for the generated MCP config
instead.

Generate a plugin directory with a manifest and `.mcp.json`, wrap
`claude` with `--plugin-dir` before user arguments, and snapshot that
wrapper directly in the tests. Keep the existing LSP support in the
generated plugin directory as well, and add coverage for the combined
MCP+LSP case plus the MCP integration merge path.

Signed-off-by: Austin Horstman <khaneliman12@gmail.com>
This commit is contained in:
Austin Horstman 2026-03-18 16:57:42 -05:00
parent d47357a4c8
commit 4fcef56c15
11 changed files with 205 additions and 39 deletions

View file

@ -3,6 +3,7 @@
claude-code-full-config = ./full-config.nix;
claude-code-lsp = ./lsp.nix;
claude-code-mcp = ./mcp.nix;
claude-code-mcp-lsp = ./mcp-lsp.nix;
claude-code-mcp-integration = ./mcp-integration.nix;
claude-code-assertion = ./assertion.nix;
claude-code-memory-management = ./memory-management.nix;

View file

@ -0,0 +1,21 @@
{
"go": {
"args": [
"serve"
],
"command": "gopls",
"extensionToLanguage": {
".go": "go"
}
},
"typescript": {
"args": [
"--stdio"
],
"command": "typescript-language-server",
"extensionToLanguage": {
".ts": "typescript",
".tsx": "typescriptreact"
}
}
}

View file

@ -1,2 +1,2 @@
#! /nix/store/00000000000000000000000000000000-bash/bin/bash -e
exec -a "$0" "/nix/store/00000000000000000000000000000000-claude-code/bin/.claude-wrapped" "$@" --plugin-dir /nix/store/00000000000000000000000000000000-claude-code-lsp-plugin
#! @bash-interactive@/bin/bash -e
exec -a "$0" "/nix/store/00000000000000000000000000000000-claude-code/bin/.claude-wrapped" --plugin-dir /nix/store/00000000000000000000000000000000-claude-code-hm-plugin "$@"

View file

@ -0,0 +1,36 @@
{
"mcpServers": {
"customTransport": {
"customOption": "value",
"timeout": 5000,
"type": "websocket",
"url": "wss://example.com/mcp"
},
"database": {
"args": [
"-y",
"@bytebase/dbhub",
"--dsn",
"postgresql://user:pass@localhost:5432/db"
],
"command": "npx",
"env": {
"DATABASE_URL": "postgresql://user:pass@localhost:5432/db"
},
"type": "stdio"
},
"filesystem": {
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/tmp"
],
"command": "npx",
"type": "stdio"
},
"github": {
"type": "http",
"url": "https://api.githubcopilot.com/mcp/"
}
}
}

View file

@ -1,2 +1,2 @@
#! /nix/store/00000000000000000000000000000000-bash/bin/bash -e
exec -a "$0" "/nix/store/00000000000000000000000000000000-claude-code/bin/.claude-wrapped" "$@" --mcp-config /nix/store/00000000000000000000000000000000-claude-code-mcp-config.json
#! @bash-interactive@/bin/bash -e
exec -a "$0" "/nix/store/00000000000000000000000000000000-claude-code/bin/.claude-wrapped" --plugin-dir /nix/store/00000000000000000000000000000000-claude-code-hm-plugin "$@"

View file

@ -0,0 +1,3 @@
{
"name": "claude-code-home-manager"
}

View file

@ -32,7 +32,14 @@
};
nmt.script = ''
normalizedWrapper=$(normalizeStorePaths home-path/bin/claude)
assertFileContent $normalizedWrapper ${./expected-lsp-wrapper}
wrapperPath="$TESTED/home-path/bin/claude"
normalizedWrapper=$(normalizeStorePaths "$wrapperPath")
assertFileContent "$normalizedWrapper" ${./expected-lsp-wrapper}
pluginDir=$(grep -o -- '--plugin-dir /nix/store/[^ ]*' "$wrapperPath")
pluginDir="''${pluginDir#--plugin-dir }"
assertFileContent "$pluginDir/.claude-plugin/plugin.json" ${./expected-plugin-manifest.json}
assertFileContent "$pluginDir/.lsp.json" ${./expected-lsp-plugin.json}
assertPathNotExists "$pluginDir/.mcp.json"
'';
}

View file

@ -13,7 +13,7 @@
};
enable = true;
enableMcpIntagretion = true;
enableMcpIntegration = true;
mcpServers = {
github = {
@ -66,7 +66,17 @@
};
nmt.script = ''
normalizedWrapper=$(normalizeStorePaths home-path/bin/claude)
assertFileContent $normalizedWrapper ${./expected-mcp-wrapper}
wrapperPath="$TESTED/home-path/bin/claude"
normalizedWrapper=$(normalizeStorePaths "$wrapperPath")
assertFileContent "$normalizedWrapper" ${./expected-mcp-wrapper}
pluginDir=$(grep -o -- '--plugin-dir /nix/store/[^ ]*' "$wrapperPath")
pluginDir="''${pluginDir#--plugin-dir }"
assertFileContent "$pluginDir/.claude-plugin/plugin.json" ${./expected-plugin-manifest.json}
assertFileRegex "$pluginDir/.mcp.json" '"github"'
assertFileRegex "$pluginDir/.mcp.json" '"database"'
assertFileRegex "$pluginDir/.mcp.json" '"/tmp"'
(! grep -q -- '/other-tmp' "$pluginDir/.mcp.json")
assertPathNotExists "$pluginDir/.lsp.json"
'';
}

View file

@ -0,0 +1,81 @@
{ config, ... }:
{
programs.claude-code = {
package = config.lib.test.mkStubPackage {
name = "claude-code";
buildScript = ''
mkdir -p $out/bin
touch $out/bin/claude
chmod 755 $out/bin/claude
'';
};
enable = true;
lspServers = {
go = {
command = "gopls";
args = [ "serve" ];
extensionToLanguage = {
".go" = "go";
};
};
typescript = {
command = "typescript-language-server";
args = [ "--stdio" ];
extensionToLanguage = {
".ts" = "typescript";
".tsx" = "typescriptreact";
};
};
};
mcpServers = {
github = {
type = "http";
url = "https://api.githubcopilot.com/mcp/";
};
filesystem = {
type = "stdio";
command = "npx";
args = [
"-y"
"@modelcontextprotocol/server-filesystem"
"/tmp"
];
};
database = {
type = "stdio";
command = "npx";
args = [
"-y"
"@bytebase/dbhub"
"--dsn"
"postgresql://user:pass@localhost:5432/db"
];
env = {
DATABASE_URL = "postgresql://user:pass@localhost:5432/db";
};
};
customTransport = {
type = "websocket";
url = "wss://example.com/mcp";
customOption = "value";
timeout = 5000;
};
};
};
nmt.script = ''
wrapperPath="$TESTED/home-path/bin/claude"
normalizedWrapper=$(normalizeStorePaths "$wrapperPath")
assertFileContent "$normalizedWrapper" ${./expected-mcp-wrapper}
test "$(grep -o -- '--plugin-dir ' "$wrapperPath" | wc -l)" -eq 1
pluginDir=$(grep -o -- '--plugin-dir /nix/store/[^ ]*' "$wrapperPath")
pluginDir="''${pluginDir#--plugin-dir }"
assertFileContent "$pluginDir/.claude-plugin/plugin.json" ${./expected-plugin-manifest.json}
assertFileContent "$pluginDir/.mcp.json" ${./expected-mcp-plugin.json}
assertFileContent "$pluginDir/.lsp.json" ${./expected-lsp-plugin.json}
'';
}

View file

@ -49,7 +49,14 @@
};
nmt.script = ''
normalizedWrapper=$(normalizeStorePaths home-path/bin/claude)
assertFileContent $normalizedWrapper ${./expected-mcp-wrapper}
wrapperPath="$TESTED/home-path/bin/claude"
normalizedWrapper=$(normalizeStorePaths "$wrapperPath")
assertFileContent "$normalizedWrapper" ${./expected-mcp-wrapper}
pluginDir=$(grep -o -- '--plugin-dir /nix/store/[^ ]*' "$wrapperPath")
pluginDir="''${pluginDir#--plugin-dir }"
assertFileContent "$pluginDir/.claude-plugin/plugin.json" ${./expected-plugin-manifest.json}
assertFileContent "$pluginDir/.mcp.json" ${./expected-mcp-plugin.json}
assertPathNotExists "$pluginDir/.lsp.json"
'';
}