Skip to content

Fix ENOENT error when installing MCP server with mcp install server.py #478

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
rossheat opened this issue Apr 10, 2025 · 0 comments
Open

Comments

@rossheat
Copy link

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:

  1. 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) ✗
  1. Open Claude Desktop
  2. 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":

server_config: dict[str, Any] = {"command": "uv", "args": args}

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant