Description
Describe the bug
When using mcp install server.py
to install a Model Context Protocol server in Claude Desktop, the installation appears successful but fails at runtime with "spawn uv ENOENT" errors. This happens because the MCP SDK hardcodes "uv" as the command without using the full path, and Claude Desktop can't find the executable when launched.
To Reproduce
Steps to reproduce the behavior:
- Install the MCP demo server with
uv run mcp install server.py
:
➜ mcp-server-demo git:(main) ✗ uv run mcp install server.py
[04/10/25 17:07:31] INFO Added server 'Demo' to Claude config claude.py:129
INFO Successfully installed Demo in Claude app cli.py:467
➜ mcp-server-demo git:(main) ✗
- Open Claude Desktop
- Observe three error notifications pop up:
- "MCP Demo: spawn uv ENOENT 2"
- "Could not connect to MCP server Demo"
- "MCP Demo: Server disconnected. For troubleshooting guidance, please visit our debugging documentation"
Expected behavior
The MCP server should start properly when Claude Desktop launches, without any ENOENT errors. The SDK should use the full path to the uv
executable rather than assuming it's in the PATH.
Screenshots
N/A
Desktop (please complete the following information):
- OS: macOS Sonoma 14.5
- Claude Desktop: 0.9.2
- modelcontextprotocol/python-sdk: 1.6.0
Smartphone (please complete the following information):
N/A
Additional context
Log Output
The following is present in mcp-server-Demo.log
:
<snip-server-pre-initialization-logs>
2025-04-10T14:46:54.177Z [Demo] [info] Initializing server...
2025-04-10T14:46:54.204Z [Demo] [error] spawn uv ENOENT {"context":"connection","stack":"Error: spawn uv ENOENT\n at ChildProcess._handle.onexit (node:internal/child_process:285:19)\n at onErrorNT (node:internal/child_process:483:16)\n at process.processTicksAndRejections (node:internal/process/task_queues:82:21)"}
2025-04-10T14:46:54.204Z [Demo] [error] spawn uv ENOENT {"stack":"Error: spawn uv ENOENT\n at ChildProcess._handle.onexit (node:internal/child_process:285:19)\n at onErrorNT (node:internal/child_process:483:16)\n at process.processTicksAndRejections (node:internal/process/task_queues:82:21)"}
<snip-server-disconnect-logs>
Root Cause Analysis
The mcp install server.py
command configures the Demo
server's command
in claude_desktop_config.json
to be "uv":
"Demo": {
"command": "uv",
"args": [
"run",
"--with",
"mcp[cli]",
"mcp",
"run",
"<full-path-to-server-py-file>"
]
}
Spawn emits the ENOENT error because 'uv' doesn't exist in the directories defined in PATH when Claude Desktop runs.
The Model Context Protocol server quickstart docs warn about this: "You may need to put the full path to the uv
executable in the command
field. You can get this by running which uv
on MacOS/Linux or where uv
on Windows."
Verification
Changing the command
from "uv" to the full path to the uv executable fixes the errors:
"Demo": {
"command": "<full-path-to-uv-executable>",
"args": [
"run",
"--with",
"mcp[cli]",
"mcp",
"run",
"<full-path-to-server-py-file>"
]
}
Code Issue
Currently, "command" is hardcoded to be "uv":
python-sdk/src/mcp/cli/claude.py
Line 120 in c4beb3e
Proposed Solution
Create a function that returns the full path to the uv executable:
import os
import subprocess
import platform
def find_uv_path() -> str | None:
"""Find the full path to the 'uv' executable across platforms"""
system = platform.system()
if system == "Windows":
try:
result = subprocess.run(["where", "uv"], capture_output=True, text=True, check=True)
paths = result.stdout.strip().split('\n')
return paths[0] if paths else None
except subprocess.CalledProcessError:
return None
else:
try:
result = subprocess.run(["which", "uv"], capture_output=True, text=True, check=True)
return result.stdout.strip()
except subprocess.CalledProcessError:
return None
Which could then be used in src/mcp/cli/claude.py
like so:
def update_claude_config(<snip>) -> bool:
<snip>
uv_path = find_uv_path()
if uv_path:
server_config: dict[str, Any] = {"command": uv_path, "args": args}
else:
server_config: dict[str, Any] = {"command": "uv", "args": args}
<snip>