As documented by Jonathan Gastón Löwenstern (hereafter, JGL), loading Claude Desktop MCP servers while using Node Version Manager (NVM) can be fraught, and there is little assistance in the community. Thankfully, Jonathon describes and solves the problems.
My modification is the ability to configure and use an NVM alias for use with Claude MCPs, without hardcoding the node version.
Caveats: This is tested and known to work on MacOS Monterey with ZSH. I have not tried other flavors of MacOS, Linux, or shells.
Like JGL's solution, we still use an npx wrapper to load MCPs within the NVM context. The change is in how we specify and control that context.
We do this in four parts:
- Move NVM initialization to a location that is sure to run for all four kinds of shells -
[Non]Login-[Non]Interactive
. In the case of ZSH (and probably all? others), this means initializing NVM in one of thezshenv
files. - Create an NVM alias to control which node version Claude uses for MCP loads.
- Invoke the NVM alias in the npx wrapper script.
- Use the npx wrapper in place of
npx
in the MCP configuration.
ZSH uses two zshenv
files, one at /etc/zshenv
(system), and one at ~/.zshenv
(personal). We use both, primarily to avoid PATH configuration conflicts.
First, /etc/zshenv
. This runs for all shells, all users. MacOS puts some stuff in /etc/zprofile
that needs to move to the system zshenv
.
/etc/zshenv
## /etc/zshenv
# MacOS puts this in /etc/zprofile originally, but it clobbers certain PATH-dependent utilities, namely NVM
if [ -x /usr/libexec/path_helper ]; then
eval `/usr/libexec/path_helper -s`
fi
This path_helper
utility configures the initial system PATH using /etc/paths
, and /etc/paths.d
. It's not clear why this would be in a zprofile
which only runs for login shells. In any case, it will now run for all shells.
Importantly, it runs before the NVM initializer, coming next.
~/.zshenv
## ~/.zshenv
function run() {
## normal .zshenv content... ##
# This is the standard NVM initialization. It's what enables you to use NVM in general, and an alias in npx-for-claude in particular.
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
## It's possible you have other NVM shims here, like the nvmrc auto changer on directory changes... ##
## ... other shell settings for interactive and non-interactive uses ... ##
}
## This next bit shuts off unwanted STDOUT output when running a non-interactive shell, such as MCPs in Claude Desktop.
## Without this redirection, any messages hitting STDOUT will foul the MCP server loads.
## Ref: https://stackoverflow.com/a/44661168/4346960
OUTPUT=1
if [[ ! -o interactive ]]; then
OUTPUT=3
eval "exec $OUTPUT<>/dev/null"
fi
run >& $OUTPUT
if [[ ! -o interactive ]]; then
eval "exec $OUTPUT>&-"
fi
The important bit of magic is the redirection for NonInteractive shells:
- Create a temporary file descriptor equivalent to /dev/null
- Redirect the output of
run
function to it - Remove the temporary file descriptor
Interactive shells output to STDOUT like normal.
NVM lets you set aliases for the versions it manages. Some are standard (or at least common,) like "default", "stable", or one of the "lts..." monikers.
We simply create our own alias for the node version Claude Desktop should use to load MCPs. You can do this at a terminal any time before running Claude Desktop, and you only have to do it once - NVM remembers aliases across shells, initializations, and reboots.
> nvm alias claude default
In the above command, "claude" is the alias you want to use in npx-for-claude
, and "default" is whatever version (or alias) you want the claude alias to be. In my case, "default" is aliased to the "stable" release, and "stable" is further aliased to the most recent lts.
The next bit is a very simple change to npx-for-claude
script. Instead of hardcoding a path to an NVM-controlled npx
, we use NVM itself to activate it.
npx-for-claude
#!/usr/bin/env zsh
nvm use claude > /dev/null 2>&1
npx "$@"
The "shebang," #!/usr/bin/env zsh
, ensures the zshenv files (system and user) are both run - so PATH is configured by path_helper
, and NVM is intitialized.
Then nvm uses our "claude" alias to set the right node/npm/npx version.
Note the output is sent to /dev/null directly, to avoid poluting STDOUT and fouling the MCP load.
This is already well-documented, both in the community and by JGL, but to quickly recap...
{
"mcpServers": {
"filesystem": {
"command": "npx-for-claude",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/Users/slhenty/Documents"
]
}
}
}
Note the command used - npx-for-claude
in lieu of npx
. This runs our wrapper script, with NVM and node properly configured.
At this point, any MCP that suggests using npx
should use npx-for-claude
. Similarly, if you choose to load MCP modules onto your system, you may need to create a similar wrapper for whatever node/npm command is used. Just include the same nvm use claude...
command as in npx-for-claude
.