diff --git a/src/fastmcp/cli/install/claude_code.py b/src/fastmcp/cli/install/claude_code.py index 5a9a98625..da475afb5 100644 --- a/src/fastmcp/cli/install/claude_code.py +++ b/src/fastmcp/cli/install/claude_code.py @@ -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 diff --git a/src/fastmcp/cli/install/claude_desktop.py b/src/fastmcp/cli/install/claude_desktop.py index 803cf5286..bd8cf395d 100644 --- a/src/fastmcp/cli/install/claude_desktop.py +++ b/src/fastmcp/cli/install/claude_desktop.py @@ -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: diff --git a/src/fastmcp/cli/install/cursor.py b/src/fastmcp/cli/install/cursor.py index 8c13c2803..dd885e5ea 100644 --- a/src/fastmcp/cli/install/cursor.py +++ b/src/fastmcp/cli/install/cursor.py @@ -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: diff --git a/src/fastmcp/cli/install/gemini_cli.py b/src/fastmcp/cli/install/gemini_cli.py index c09016990..8acaa413e 100644 --- a/src/fastmcp/cli/install/gemini_cli.py +++ b/src/fastmcp/cli/install/gemini_cli.py @@ -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 diff --git a/src/fastmcp/cli/install/mcp_json.py b/src/fastmcp/cli/install/mcp_json.py index 7dcb0c0da..95ccb3ca3 100644 --- a/src/fastmcp/cli/install/mcp_json.py +++ b/src/fastmcp/cli/install/mcp_json.py @@ -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: diff --git a/src/fastmcp/client/transports.py b/src/fastmcp/client/transports.py index 8492a5125..1016d35e0 100644 --- a/src/fastmcp/client/transports.py +++ b/src/fastmcp/client/transports.py @@ -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}" ) diff --git a/src/fastmcp/utilities/mcp_server_config/v1/environments/uv.py b/src/fastmcp/utilities/mcp_server_config/v1/environments/uv.py index 6f71cd3ac..a88965435 100644 --- a/src/fastmcp/utilities/mcp_server_config/v1/environments/uv.py +++ b/src/fastmcp/utilities/mcp_server_config/v1/environments/uv.py @@ -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) diff --git a/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py b/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py index c269d0eda..1345d7f59 100644 --- a/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +++ b/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py @@ -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 diff --git a/tests/cli/test_config.py b/tests/cli/test_config.py index 7865e45c5..1a5d307eb 100644 --- a/tests/cli/test_config.py +++ b/tests/cli/test_config.py @@ -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:] diff --git a/tests/cli/test_mcp_server_config_integration.py b/tests/cli/test_mcp_server_config_integration.py index 05a98687c..219532b53 100644 --- a/tests/cli/test_mcp_server_config_integration.py +++ b/tests/cli/test_mcp_server_config_integration.py @@ -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: diff --git a/tests/client/transports/test_uv_transport.py b/tests/client/transports/test_uv_transport.py index 45a2c3cfc..020f084ba 100644 --- a/tests/client/transports/test_uv_transport.py +++ b/tests/client/transports/test_uv_transport.py @@ -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, ) ) diff --git a/tests/utilities/test_cli.py b/tests/utilities/test_cli.py index a14aab2f5..f463c2913 100644 --- a/tests/utilities/test_cli.py +++ b/tests/utilities/test_cli.py @@ -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):