Skip to content

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

Open
@rossheat

Description

@rossheat

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>

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions