Fix path resolution in install commands (#1951)

This commit is contained in:
Jeremiah Lowin 2025-09-29 20:04:12 -04:00 committed by GitHub
commit 3a21e7b9b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 64 additions and 55 deletions

View file

@ -110,9 +110,9 @@ def install_claude_code(
env_config = UVEnvironment(
python=python_version,
dependencies=(with_packages or []) + ["fastmcp"],
requirements=str(with_requirements) if with_requirements else None,
project=str(project) if project else None,
editable=[str(p) for p in with_editable] if with_editable else None,
requirements=with_requirements,
project=project,
editable=with_editable,
)
# Build server spec from parsed components

View file

@ -76,9 +76,9 @@ def install_claude_desktop(
env_config = UVEnvironment(
python=python_version,
dependencies=(with_packages or []) + ["fastmcp"],
requirements=str(with_requirements) if with_requirements else None,
project=str(project) if project else None,
editable=[str(p) for p in with_editable] if with_editable else None,
requirements=with_requirements,
project=project,
editable=with_editable,
)
# Build server spec from parsed components
if server_object:

View file

@ -110,9 +110,9 @@ def install_cursor_workspace(
env_config = UVEnvironment(
python=python_version,
dependencies=(with_packages or []) + ["fastmcp"],
requirements=str(with_requirements.resolve()) if with_requirements else None,
project=str(project.resolve()) if project else None,
editable=[str(p.resolve()) for p in with_editable] if with_editable else None,
requirements=with_requirements,
project=project,
editable=with_editable,
)
# Build server spec from parsed components
if server_object:
@ -180,9 +180,9 @@ def install_cursor(
env_config = UVEnvironment(
python=python_version,
dependencies=(with_packages or []) + ["fastmcp"],
requirements=str(with_requirements.resolve()) if with_requirements else None,
project=str(project.resolve()) if project else None,
editable=[str(p.resolve()) for p in with_editable] if with_editable else None,
requirements=with_requirements,
project=project,
editable=with_editable,
)
# Build server spec from parsed components
if server_object:

View file

@ -107,9 +107,9 @@ def install_gemini_cli(
env_config = UVEnvironment(
python=python_version,
dependencies=(with_packages or []) + ["fastmcp"],
requirements=str(with_requirements) if with_requirements else None,
project=str(project) if project else None,
editable=[str(p) for p in with_editable] if with_editable else None,
requirements=with_requirements,
project=project,
editable=with_editable,
)
# Build server spec from parsed components

View file

@ -51,9 +51,9 @@ def install_mcp_json(
env_config = UVEnvironment(
python=python_version,
dependencies=(with_packages or []) + ["fastmcp"],
requirements=str(with_requirements) if with_requirements else None,
project=str(project) if project else None,
editable=[str(p) for p in with_editable] if with_editable else None,
requirements=with_requirements,
project=project,
editable=with_editable,
)
# Build server spec from parsed components
if server_object:

View file

@ -583,15 +583,15 @@ class UvStdioTransport(StdioTransport):
command: str,
args: list[str] | None = None,
module: bool = False,
project_directory: str | None = None,
project_directory: Path | None = None,
python_version: str | None = None,
with_packages: list[str] | None = None,
with_requirements: str | None = None,
with_requirements: Path | None = None,
env_vars: dict[str, str] | None = None,
keep_alive: bool | None = None,
):
# Basic validation
if project_directory and not Path(project_directory).exists():
if project_directory and not project_directory.exists():
raise NotADirectoryError(
f"Project directory not found: {project_directory}"
)

View file

@ -28,19 +28,19 @@ class UVEnvironment(Environment):
examples=[["fastmcp>=2.0,<3", "httpx", "pandas>=2.0"]],
)
requirements: str | None = Field(
requirements: Path | None = Field(
default=None,
description="Path to requirements.txt file",
examples=["requirements.txt", "../requirements/prod.txt"],
)
project: str | None = Field(
project: Path | None = Field(
default=None,
description="Path to project directory containing pyproject.toml",
examples=[".", "../my-project"],
)
editable: list[str] | None = Field(
editable: list[Path] | None = Field(
default=None,
description="Directories to install in editable mode",
examples=[[".", "../my-package"], ["/path/to/package"]],
@ -64,7 +64,7 @@ class UVEnvironment(Environment):
# Add project if specified
if self.project:
args.extend(["--project", str(self.project)])
args.extend(["--project", str(self.project.resolve())])
# Add Python version if specified (only if no project, as project has its own Python)
if self.python and not self.project:
@ -78,12 +78,12 @@ class UVEnvironment(Environment):
# Add requirements file
if self.requirements:
args.extend(["--with-requirements", str(self.requirements)])
args.extend(["--with-requirements", str(self.requirements.resolve())])
# Add editable packages
if self.editable:
for editable_path in self.editable:
args.extend(["--with-editable", str(editable_path)])
args.extend(["--with-editable", str(editable_path.resolve())])
# Add the command
args.extend(command)

View file

@ -291,9 +291,9 @@ class MCPServerConfig(BaseModel):
environment = UVEnvironment(
python=python,
dependencies=dependencies,
requirements=requirements,
project=project,
editable=[editable] if editable else None,
requirements=Path(requirements) if requirements else None,
project=Path(project) if project else None,
editable=[Path(editable)] if editable else None,
)
# Build deployment config if any deployment args provided

View file

@ -66,9 +66,10 @@ class TestEnvironment:
env = config.environment
assert env.python == "3.12"
assert env.dependencies == ["requests", "numpy>=2.0"]
assert env.requirements == "requirements.txt"
assert env.project == "."
assert env.editable == ["../my-package"]
# Paths are stored as Path objects
assert env.requirements == Path("requirements.txt")
assert env.project == Path(".")
assert env.editable == [Path("../my-package")]
def test_needs_uv(self):
"""Test needs_uv() method."""
@ -112,12 +113,16 @@ class TestEnvironment:
assert "--python" not in cmd
assert "3.12" not in cmd
assert "--project" in cmd
assert "." in cmd
# Project path should be resolved to absolute path
project_idx = cmd.index("--project")
assert Path(cmd[project_idx + 1]).is_absolute()
assert "--with" in cmd
assert "requests" in cmd
assert "numpy" in cmd
assert "--with-requirements" in cmd
assert "requirements.txt" in cmd
# Requirements path should be resolved to absolute path
req_idx = cmd.index("--with-requirements")
assert Path(cmd[req_idx + 1]).is_absolute()
# Command args should be at the end
assert "fastmcp" in cmd[-3:]
assert "run" in cmd[-2:]

View file

@ -234,10 +234,11 @@ class TestPathResolution:
assert config.environment is not None
uv_cmd = config.environment.build_command(["fastmcp", "run"])
# Should include requirements file
# Should include requirements file with absolute path
assert "--with-requirements" in uv_cmd
req_idx = uv_cmd.index("--with-requirements") + 1
assert uv_cmd[req_idx] == "requirements.txt"
assert Path(uv_cmd[req_idx]).is_absolute()
assert Path(uv_cmd[req_idx]).name == "requirements.txt"
class TestConfigValidation:

View file

@ -84,7 +84,7 @@ async def test_uv_transport_module():
with_packages=["fastmcp"],
command="my_module",
module=True,
project_directory=tmpdir,
project_directory=Path(tmpdir),
keep_alive=False,
)
)

View file

@ -1,5 +1,7 @@
"""Tests for CLI utility functions."""
from pathlib import Path
from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
@ -16,14 +18,14 @@ class TestEnvironmentBuildUVRunCommand:
def test_build_uv_run_command_with_editable(self):
"""Test building uv command with editable package."""
editable_path = "/path/to/package"
editable_path = Path("/path/to/package")
env = UVEnvironment(editable=[editable_path])
cmd = env.build_command(["fastmcp", "run", "server.py"])
expected = [
"uv",
"run",
"--with-editable",
editable_path,
str(editable_path.resolve()),
"fastmcp",
"run",
"server.py",
@ -64,14 +66,14 @@ class TestEnvironmentBuildUVRunCommand:
def test_build_uv_run_command_with_requirements(self):
"""Test building uv command with requirements file."""
requirements_path = "/path/to/requirements.txt"
requirements_path = Path("/path/to/requirements.txt")
env = UVEnvironment(requirements=requirements_path)
cmd = env.build_command(["fastmcp", "run", "server.py"])
expected = [
"uv",
"run",
"--with-requirements",
requirements_path,
str(requirements_path.resolve()),
"fastmcp",
"run",
"server.py",
@ -80,14 +82,14 @@ class TestEnvironmentBuildUVRunCommand:
def test_build_uv_run_command_with_project(self):
"""Test building uv command with project directory."""
project_path = "/path/to/project"
project_path = Path("/path/to/project")
env = UVEnvironment(project=project_path)
cmd = env.build_command(["fastmcp", "run", "server.py"])
expected = [
"uv",
"run",
"--project",
project_path,
str(project_path.resolve()),
"fastmcp",
"run",
"server.py",
@ -96,8 +98,8 @@ class TestEnvironmentBuildUVRunCommand:
def test_build_uv_run_command_with_everything(self):
"""Test building uv command with all options."""
requirements_path = "/path/to/requirements.txt"
editable_path = "/local/pkg"
requirements_path = Path("/path/to/requirements.txt")
editable_path = Path("/local/pkg")
env = UVEnvironment(
python="3.10",
dependencies=["pandas", "numpy"],
@ -115,9 +117,9 @@ class TestEnvironmentBuildUVRunCommand:
"--with",
"pandas",
"--with-requirements",
requirements_path,
str(requirements_path.resolve()),
"--with-editable",
editable_path,
str(editable_path.resolve()),
"fastmcp",
"run",
"server.py",
@ -129,23 +131,24 @@ class TestEnvironmentBuildUVRunCommand:
def test_build_uv_run_command_project_with_extras(self):
"""Test that project flag works with additional dependencies."""
project_path = "/path/to/project"
project_path = Path("/path/to/project")
editable_path = Path("/pkg")
env = UVEnvironment(
project=project_path,
python="3.10", # Should be ignored with project
dependencies=["pandas"], # Should be added on top of project
editable=["/pkg"], # Should be added on top of project
editable=[editable_path], # Should be added on top of project
)
cmd = env.build_command(["fastmcp", "run", "server.py"])
expected = [
"uv",
"run",
"--project",
project_path,
str(project_path.resolve()),
"--with",
"pandas",
"--with-editable",
"/pkg",
str(editable_path.resolve()),
"fastmcp",
"run",
"server.py",
@ -168,17 +171,17 @@ class TestEnvironmentNeedsUV:
def test_needs_uv_with_requirements(self):
"""Test that needs_uv returns True with requirements."""
env = UVEnvironment(requirements="/path/to/requirements.txt")
env = UVEnvironment(requirements=Path("/path/to/requirements.txt"))
assert env._must_run_with_uv() is True
def test_needs_uv_with_project(self):
"""Test that needs_uv returns True with project."""
env = UVEnvironment(project="/path/to/project")
env = UVEnvironment(project=Path("/path/to/project"))
assert env._must_run_with_uv() is True
def test_needs_uv_with_editable(self):
"""Test that needs_uv returns True with editable."""
env = UVEnvironment(editable=["/pkg"])
env = UVEnvironment(editable=[Path("/pkg")])
assert env._must_run_with_uv() is True
def test_needs_uv_empty(self):