This commit is contained in:
Jeremiah Lowin 2025-08-23 19:54:25 -04:00
commit 007af5d9b3
15 changed files with 391 additions and 392 deletions

View file

@ -25,7 +25,7 @@ repos:
- repo: local
hooks:
- id: ty
name: type check
name: ty check
entry: uv run ty check
language: system
types: [python]
@ -37,4 +37,5 @@ repos:
rev: v6.0.0
hooks:
- id: no-commit-to-branch
name: prevent commits to main
args: [--branch, main]

View file

@ -6,11 +6,11 @@ from fastmcp.settings import Settings
from fastmcp.utilities.logging import configure_logging as _configure_logging
settings = Settings()
# if False:
_configure_logging(
level=settings.log_level,
enable_rich_tracebacks=settings.enable_rich_tracebacks,
)
if settings.log_enabled:
_configure_logging(
level=settings.log_level,
enable_rich_tracebacks=settings.enable_rich_tracebacks,
)
from fastmcp.server.server import FastMCP
from fastmcp.server.context import Context

View file

@ -6,6 +6,7 @@ import sys
from pathlib import Path
from typing import Any
from fastmcp.utilities.cli import build_uv_run_args
from fastmcp.utilities.logging import get_logger
logger = get_logger(__name__)
@ -89,20 +90,11 @@ def update_claude_config(
else:
env_vars = existing_env
# Build uv run command
args = ["run"]
# Collect all packages in a set to deduplicate
packages = {"fastmcp"}
if with_packages:
packages.update(pkg for pkg in with_packages if pkg)
# Add all packages with --with
for pkg in sorted(packages):
args.extend(["--with", pkg])
if with_editable:
args.extend(["--with-editable", str(with_editable)])
# Build uv run command using centralized function
args = build_uv_run_args(
with_editable=with_editable,
with_packages=with_packages,
)
# Convert file path to absolute before adding to command
# Split off any :object suffix first

View file

@ -19,6 +19,7 @@ import fastmcp
from fastmcp.cli import run as run_module
from fastmcp.cli.install import install_app
from fastmcp.server.server import FastMCP
from fastmcp.utilities.cli import build_uv_command
from fastmcp.utilities.inspect import FastMCPInfo, inspect_fastmcp
from fastmcp.utilities.logging import get_logger
@ -57,46 +58,8 @@ def _parse_env_var(env_var: str) -> tuple[str, str]:
return key.strip(), value.strip()
def _build_uv_command(
server_spec: str,
with_editable: Path | None = None,
with_packages: list[str] | None = None,
no_banner: bool = False,
python_version: str | None = None,
with_requirements: Path | None = None,
project: Path | None = None,
) -> list[str]:
"""Build the uv run command that runs a MCP server through mcp run."""
cmd = ["uv", "run"]
# Add Python version if specified
if python_version:
cmd.extend(["--python", python_version])
# Add project if specified
if project:
cmd.extend(["--project", str(project)])
cmd.extend(["--with", "fastmcp"])
if with_editable:
cmd.extend(["--with-editable", str(with_editable)])
if with_packages:
for pkg in with_packages:
if pkg:
cmd.extend(["--with", pkg])
if with_requirements:
cmd.extend(["--with-requirements", str(with_requirements)])
# Add mcp run command
cmd.extend(["fastmcp", "run", server_spec])
if no_banner:
cmd.append("--no-banner")
return cmd
# The _build_uv_command function has been moved to cli/utils.py
# and is now imported as build_uv_command
@app.command
@ -302,16 +265,18 @@ async def dev(
if inspector_version:
inspector_cmd += f"@{inspector_version}"
uv_cmd = _build_uv_command(
uv_cmd = build_uv_command(
server_spec,
with_editable,
with_packages,
no_banner=True,
with_editable=with_editable,
with_packages=with_packages,
python_version=python,
with_requirements=with_requirements,
project=project,
)
# Add --no-banner flag for dev command
uv_cmd.append("--no-banner")
# Run the MCP Inspector command with shell=True on Windows
shell = sys.platform == "win32"
process = subprocess.run(

View file

@ -9,6 +9,7 @@ from typing import Annotated
import cyclopts
from rich import print
from fastmcp.utilities.cli import build_uv_run_args
from fastmcp.utilities.logging import get_logger
from .shared import process_common_args
@ -106,31 +107,14 @@ def install_claude_code(
)
return False
# Build uv run command
args = ["run"]
# Add Python version if specified
if python_version:
args.extend(["--python", python_version])
# Add project if specified
if project:
args.extend(["--project", str(project)])
# Collect all packages in a set to deduplicate
packages = {"fastmcp"}
if with_packages:
packages.update(pkg for pkg in with_packages if pkg)
# Add all packages with --with
for pkg in sorted(packages):
args.extend(["--with", pkg])
if with_editable:
args.extend(["--with-editable", str(with_editable)])
if with_requirements:
args.extend(["--with-requirements", str(with_requirements)])
# Build uv run command using centralized function
args = build_uv_run_args(
with_editable=with_editable,
with_packages=with_packages,
python_version=python_version,
with_requirements=with_requirements,
project=project,
)
# Build server spec from parsed components
if server_object:

View file

@ -9,6 +9,7 @@ import cyclopts
from rich import print
from fastmcp.mcp_config import StdioMCPServer, update_config_file
from fastmcp.utilities.cli import build_uv_run_args
from fastmcp.utilities.logging import get_logger
from .shared import process_common_args
@ -72,31 +73,14 @@ def install_claude_desktop(
config_file = config_dir / "claude_desktop_config.json"
# Build uv run command
args = ["run"]
# Add Python version if specified
if python_version:
args.extend(["--python", python_version])
# Add project if specified
if project:
args.extend(["--project", str(project)])
# Collect all packages in a set to deduplicate
packages = {"fastmcp"}
if with_packages:
packages.update(pkg for pkg in with_packages if pkg)
# Add all packages with --with
for pkg in sorted(packages):
args.extend(["--with", pkg])
if with_editable:
args.extend(["--with-editable", str(with_editable)])
if with_requirements:
args.extend(["--with-requirements", str(with_requirements)])
# Build uv run command using centralized function
args = build_uv_run_args(
with_editable=with_editable,
with_packages=with_packages,
python_version=python_version,
with_requirements=with_requirements,
project=project,
)
# Build server spec from parsed components
if server_object:

View file

@ -10,6 +10,7 @@ import cyclopts
from rich import print
from fastmcp.mcp_config import StdioMCPServer, update_config_file
from fastmcp.utilities.cli import build_uv_run_args
from fastmcp.utilities.logging import get_logger
from .shared import process_common_args
@ -106,31 +107,14 @@ def install_cursor_workspace(
config_file = cursor_dir / "mcp.json"
# Build uv run command
args = ["run"]
# Add Python version if specified
if python_version:
args.extend(["--python", python_version])
# Add project if specified
if project:
args.extend(["--project", str(project)])
# Collect all packages in a set to deduplicate
packages = {"fastmcp"}
if with_packages:
packages.update(pkg for pkg in with_packages if pkg)
# Add all packages with --with
for pkg in sorted(packages):
args.extend(["--with", pkg])
if with_editable:
args.extend(["--with-editable", str(with_editable)])
if with_requirements:
args.extend(["--with-requirements", str(with_requirements)])
# Build uv run command using centralized function
args = build_uv_run_args(
with_editable=with_editable,
with_packages=with_packages,
python_version=python_version,
with_requirements=with_requirements,
project=project,
)
# Build server spec from parsed components
if server_object:
@ -194,31 +178,14 @@ def install_cursor(
Returns:
True if installation was successful, False otherwise
"""
# Build uv run command
args = ["run"]
# Add Python version if specified
if python_version:
args.extend(["--python", python_version])
# Add project if specified
if project:
args.extend(["--project", str(project)])
# Collect all packages in a set to deduplicate
packages = {"fastmcp"}
if with_packages:
packages.update(pkg for pkg in with_packages if pkg)
# Add all packages with --with
for pkg in sorted(packages):
args.extend(["--with", pkg])
if with_editable:
args.extend(["--with-editable", str(with_editable)])
if with_requirements:
args.extend(["--with-requirements", str(with_requirements)])
# Build uv run command using centralized function
args = build_uv_run_args(
with_editable=with_editable,
with_packages=with_packages,
python_version=python_version,
with_requirements=with_requirements,
project=project,
)
# Build server spec from parsed components
if server_object:

View file

@ -9,6 +9,7 @@ import cyclopts
import pyperclip
from rich import print
from fastmcp.utilities.cli import build_uv_run_args
from fastmcp.utilities.logging import get_logger
from .shared import process_common_args
@ -47,31 +48,14 @@ def install_mcp_json(
True if generation was successful, False otherwise
"""
try:
# Build uv run command
args = ["run"]
# Add Python version if specified
if python_version:
args.extend(["--python", python_version])
# Add project if specified
if project:
args.extend(["--project", str(project)])
# Collect all packages in a set to deduplicate
packages = {"fastmcp"}
if with_packages:
packages.update(pkg for pkg in with_packages if pkg)
# Add all packages with --with
for pkg in sorted(packages):
args.extend(["--with", pkg])
if with_editable:
args.extend(["--with-editable", str(with_editable)])
if with_requirements:
args.extend(["--with-requirements", str(with_requirements)])
# Build uv run command using centralized function
args = build_uv_run_args(
with_editable=with_editable,
with_packages=with_packages,
python_version=python_version,
with_requirements=with_requirements,
project=project,
)
# Build server spec from parsed components
if server_object:

View file

@ -13,6 +13,7 @@ from typing import Any, Literal
from mcp.server.fastmcp import FastMCP as FastMCP1x
from fastmcp.server.server import FastMCP
from fastmcp.utilities.cli import build_uv_command
from fastmcp.utilities.fastmcp_config import (
DeploymentConfig,
EntrypointConfig,
@ -255,31 +256,14 @@ def run_with_uv(
port = merged_deploy["port"]
path = merged_deploy["path"]
log_level = merged_deploy["log_level"]
cmd = ["uv", "run"]
# Add Python version if specified
if python_version:
cmd.extend(["--python", python_version])
# Add project if specified
if project:
cmd.extend(["--project", str(project)])
# Add fastmcp package
cmd.extend(["--with", "fastmcp"])
# Add additional packages
if with_packages:
for pkg in with_packages:
if pkg:
cmd.extend(["--with", pkg])
# Add requirements file
if with_requirements:
cmd.extend(["--with-requirements", str(with_requirements)])
# Add fastmcp run command
cmd.extend(["fastmcp", "run", server_spec])
# Build uv command using centralized function
cmd = build_uv_command(
server_spec,
with_packages=with_packages,
python_version=python_version,
with_requirements=with_requirements,
project=project,
)
# Add transport options
if transport:

View file

@ -146,6 +146,7 @@ class Settings(BaseSettings):
test_mode: bool = False
log_enabled: bool = True
log_level: LOG_LEVEL = "INFO"
@field_validator("log_level", mode="before")
@ -314,7 +315,7 @@ class Settings(BaseSettings):
Whether to include FastMCP meta in the server's MCP responses.
If True, a `_fastmcp` key will be added to the `meta` field of
all MCP component responses. This key will contain a dict of
various FastMCP-specific metadata, such as tags.
various FastMCP-specific metadata, such as tags.
"""
),
),

View file

@ -1,6 +1,7 @@
from __future__ import annotations
from importlib.metadata import version
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal
from rich.align import Align
@ -109,3 +110,119 @@ def log_server_banner(
console = Console(stderr=True)
console.print(Group("\n", panel, "\n"))
def build_uv_command(
server_spec: str,
*,
with_editable: Path | None = None,
with_packages: list[str] | None = None,
python_version: str | None = None,
with_requirements: Path | None = None,
project: Path | None = None,
) -> list[str]:
"""Build a uv run command for running a FastMCP server.
This centralized function ensures consistent path resolution and command building
across all CLI commands.
Args:
server_spec: Server specification (file path, optionally with :object)
with_editable: Directory to install in editable mode
with_packages: Additional packages to install
python_version: Python version to use (e.g., "3.10", "3.11")
with_requirements: Requirements file to install from
project: Project directory to run within
Returns:
List of command arguments for subprocess execution
"""
cmd = ["uv", "run"]
# Add Python version if specified
if python_version:
cmd.extend(["--python", python_version])
# Add project if specified - resolve to absolute path
if project:
cmd.extend(["--project", str(project.expanduser().resolve())])
# Always include fastmcp
cmd.extend(["--with", "fastmcp"])
# Add additional packages
if with_packages:
# Deduplicate and sort packages for consistency
packages = set(pkg for pkg in with_packages if pkg)
for pkg in sorted(packages):
cmd.extend(["--with", pkg])
# Add editable directory - resolve to absolute path
if with_editable:
cmd.extend(["--with-editable", str(with_editable.expanduser().resolve())])
# Add requirements file - resolve to absolute path
if with_requirements:
cmd.extend(
["--with-requirements", str(with_requirements.expanduser().resolve())]
)
# Add fastmcp run command
cmd.extend(["fastmcp", "run", server_spec])
return cmd
def build_uv_run_args(
*,
with_editable: Path | None = None,
with_packages: list[str] | None = None,
python_version: str | None = None,
with_requirements: Path | None = None,
project: Path | None = None,
) -> list[str]:
"""Build just the uv run arguments without the server spec.
This is useful for install commands that need to build the args array
without the full command structure.
Args:
with_editable: Directory to install in editable mode
with_packages: Additional packages to install (fastmcp will be added automatically)
python_version: Python version to use
with_requirements: Requirements file to install from
project: Project directory to run within
Returns:
List of arguments starting with "run"
"""
args = ["run"]
# Add Python version if specified
if python_version:
args.extend(["--python", python_version])
# Add project if specified - resolve to absolute path
if project:
args.extend(["--project", str(project.expanduser().resolve())])
# Collect all packages in a set to deduplicate
packages = {"fastmcp"}
if with_packages:
packages.update(pkg for pkg in with_packages if pkg)
# Add all packages with --with
for pkg in sorted(packages):
args.extend(["--with", pkg])
# Add editable directory - resolve to absolute path
if with_editable:
args.extend(["--with-editable", str(with_editable.expanduser().resolve())])
# Add requirements file - resolve to absolute path
if with_requirements:
args.extend(
["--with-requirements", str(with_requirements.expanduser().resolve())]
)
return args

View file

@ -4,7 +4,7 @@ from unittest.mock import Mock, patch
import pytest
from fastmcp.cli.cli import _build_uv_command, _parse_env_var, app
from fastmcp.cli.cli import _parse_env_var, app
class TestMainCLI:
@ -34,150 +34,6 @@ class TestMainCLI:
_parse_env_var("INVALID_FORMAT")
assert exc_info.value.code == 1
def test_build_uv_command_basic(self):
"""Test building basic uv command."""
cmd = _build_uv_command("server.py")
expected = ["uv", "run", "--with", "fastmcp", "fastmcp", "run", "server.py"]
assert cmd == expected
def test_build_uv_command_with_editable(self):
"""Test building uv command with editable package."""
editable_path = Path("/path/to/package")
cmd = _build_uv_command("server.py", with_editable=editable_path)
expected = [
"uv",
"run",
"--with",
"fastmcp",
"--with-editable",
str(editable_path),
"fastmcp",
"run",
"server.py",
]
assert cmd == expected
def test_build_uv_command_with_packages(self):
"""Test building uv command with additional packages."""
cmd = _build_uv_command("server.py", with_packages=["pkg1", "pkg2"])
expected = [
"uv",
"run",
"--with",
"fastmcp",
"--with",
"pkg1",
"--with",
"pkg2",
"fastmcp",
"run",
"server.py",
]
assert cmd == expected
def test_build_uv_command_no_banner(self):
"""Test building uv command with no banner flag."""
cmd = _build_uv_command("server.py", no_banner=True)
expected = [
"uv",
"run",
"--with",
"fastmcp",
"fastmcp",
"run",
"server.py",
"--no-banner",
]
assert cmd == expected
def test_build_uv_command_with_python_version(self):
"""Test building uv command with Python version."""
cmd = _build_uv_command("server.py", python_version="3.11")
expected = [
"uv",
"run",
"--python",
"3.11",
"--with",
"fastmcp",
"fastmcp",
"run",
"server.py",
]
assert cmd == expected
def test_build_uv_command_with_project(self):
"""Test building uv command with project directory."""
project_path = Path("/path/to/project")
cmd = _build_uv_command("server.py", project=project_path)
expected = [
"uv",
"run",
"--project",
str(project_path),
"--with",
"fastmcp",
"fastmcp",
"run",
"server.py",
]
assert cmd == expected
def test_build_uv_command_with_requirements(self):
"""Test building uv command with requirements file."""
req_path = Path("requirements.txt")
cmd = _build_uv_command("server.py", with_requirements=req_path)
expected = [
"uv",
"run",
"--with",
"fastmcp",
"--with-requirements",
"requirements.txt",
"fastmcp",
"run",
"server.py",
]
assert cmd == expected
def test_build_uv_command_with_all_options(self):
"""Test building uv command with all options."""
project_path = Path("/my/project")
editable_path = Path("/local/pkg")
requirements_path = Path("reqs.txt")
cmd = _build_uv_command(
"server.py",
python_version="3.10",
project=project_path,
with_packages=["pandas", "numpy"],
with_requirements=requirements_path,
with_editable=editable_path,
no_banner=True,
)
expected = [
"uv",
"run",
"--python",
"3.10",
"--project",
str(project_path),
"--with",
"fastmcp",
"--with-editable",
str(editable_path),
"--with",
"pandas",
"--with",
"numpy",
"--with-requirements",
str(requirements_path),
"fastmcp",
"run",
"server.py",
"--no-banner",
]
assert cmd == expected
class TestVersionCommand:
"""Test the version command."""

View file

@ -255,9 +255,10 @@ class TestInstallCursor:
config_data = json.loads(decoded)
assert "--with-editable" in config_data["args"]
# Check for the editable path in a platform-agnostic way
editable_path_str = str(Path("/local/package"))
assert editable_path_str in config_data["args"]
# Check that the path was resolved (should be absolute)
editable_idx = config_data["args"].index("--with-editable") + 1
resolved_path = config_data["args"][editable_idx]
assert Path(resolved_path).is_absolute()
assert "server.py:custom_app" in " ".join(config_data["args"])
@patch("fastmcp.cli.install.cursor.open_deeplink")

View file

@ -65,18 +65,12 @@ class TestRunWithUv:
assert exc_info.value.code == 0
cmd = mock_run.call_args[0][0]
expected = [
"uv",
"run",
"--project",
str(Path("/my/project")),
"--with",
"fastmcp",
"fastmcp",
"run",
"server.py",
]
assert cmd == expected
# Check the basic structure
assert cmd[:3] == ["uv", "run", "--project"]
# Check that the project path is absolute
assert Path(cmd[3]).is_absolute()
# Check the rest of the command
assert cmd[4:] == ["--with", "fastmcp", "fastmcp", "run", "server.py"]
@patch("subprocess.run")
def test_run_with_uv_with_packages(self, mock_run):
@ -95,9 +89,9 @@ class TestRunWithUv:
"--with",
"fastmcp",
"--with",
"pandas",
"numpy", # sorted alphabetically
"--with",
"numpy",
"pandas", # sorted alphabetically
"fastmcp",
"run",
"server.py",
@ -122,7 +116,7 @@ class TestRunWithUv:
"--with",
"fastmcp",
"--with-requirements",
"requirements.txt",
str(req_path.expanduser().resolve()), # resolved to absolute path
"fastmcp",
"run",
"server.py",
@ -190,19 +184,16 @@ class TestRunWithUv:
assert exc_info.value.code == 0
cmd = mock_run.call_args[0][0]
expected = [
"uv",
"run",
"--python",
"3.10",
"--project",
str(Path("/workspace")),
"--with",
"fastmcp",
"--with",
"pandas",
"--with-requirements",
"reqs.txt",
# Check the structure piece by piece to be platform-agnostic
assert cmd[:5] == ["uv", "run", "--python", "3.10", "--project"]
# Check project path is absolute
assert Path(cmd[5]).is_absolute()
assert cmd[6:10] == ["--with", "fastmcp", "--with", "pandas"]
assert cmd[10] == "--with-requirements"
# Check requirements path is absolute
assert Path(cmd[11]).is_absolute()
assert cmd[12:] == [
"fastmcp",
"run",
"server.py",
@ -212,7 +203,6 @@ class TestRunWithUv:
"9000",
"--no-banner",
]
assert cmd == expected
@patch("subprocess.run")
def test_run_with_uv_error_handling(self, mock_run):

173
tests/utilities/test_cli.py Normal file
View file

@ -0,0 +1,173 @@
from pathlib import Path
from fastmcp.utilities.cli import build_uv_command
class TestBuildUVCommand:
"""Test the build_uv_command function."""
def test_build_uv_command_basic(self):
"""Test building basic uv command."""
cmd = build_uv_command("server.py")
expected = ["uv", "run", "--with", "fastmcp", "fastmcp", "run", "server.py"]
assert cmd == expected
def test_build_uv_command_with_editable(self):
"""Test building uv command with editable package."""
editable_path = Path("/path/to/package")
cmd = build_uv_command("server.py", with_editable=editable_path)
expected = [
"uv",
"run",
"--with",
"fastmcp",
"--with-editable",
str(editable_path.expanduser().resolve()),
"fastmcp",
"run",
"server.py",
]
assert cmd == expected
def test_build_uv_command_with_packages(self):
"""Test building uv command with additional packages."""
cmd = build_uv_command("server.py", with_packages=["pkg1", "pkg2"])
expected = [
"uv",
"run",
"--with",
"fastmcp",
"--with",
"pkg1",
"--with",
"pkg2",
"fastmcp",
"run",
"server.py",
]
assert cmd == expected
def test_build_uv_command_with_python_version(self):
"""Test building uv command with Python version."""
cmd = build_uv_command("server.py", python_version="3.11")
expected = [
"uv",
"run",
"--python",
"3.11",
"--with",
"fastmcp",
"fastmcp",
"run",
"server.py",
]
assert cmd == expected
def test_build_uv_command_with_project(self):
"""Test building uv command with project directory."""
project_path = Path("/path/to/project")
cmd = build_uv_command("server.py", project=project_path)
expected = [
"uv",
"run",
"--project",
str(project_path.expanduser().resolve()),
"--with",
"fastmcp",
"fastmcp",
"run",
"server.py",
]
assert cmd == expected
def test_build_uv_command_with_requirements(self):
"""Test building uv command with requirements file."""
req_path = Path("requirements.txt")
cmd = build_uv_command("server.py", with_requirements=req_path)
expected = [
"uv",
"run",
"--with",
"fastmcp",
"--with-requirements",
str(req_path.expanduser().resolve()),
"fastmcp",
"run",
"server.py",
]
assert cmd == expected
def test_build_uv_command_with_all_options(self):
"""Test building uv command with all options."""
project_path = Path("/my/project")
editable_path = Path("/local/pkg")
requirements_path = Path("reqs.txt")
cmd = build_uv_command(
"server.py",
python_version="3.10",
project=project_path,
with_packages=["pandas", "numpy"],
with_requirements=requirements_path,
with_editable=editable_path,
)
expected = [
"uv",
"run",
"--python",
"3.10",
"--project",
str(project_path.expanduser().resolve()),
"--with",
"fastmcp",
"--with",
"numpy",
"--with",
"pandas",
"--with-editable",
str(editable_path.expanduser().resolve()),
"--with-requirements",
str(requirements_path.expanduser().resolve()),
"fastmcp",
"run",
"server.py",
]
assert cmd == expected
def test_with_editable_resolves_dot(self):
"""Test that '.' in with_editable becomes absolute."""
cmd = build_uv_command("server.py", with_editable=Path("."))
idx = cmd.index("--with-editable") + 1
assert Path(cmd[idx]).is_absolute()
def test_with_editable_resolves_tilde(self):
"""Test that '~' in with_editable is expanded."""
cmd = build_uv_command("server.py", with_editable=Path("~/project"))
idx = cmd.index("--with-editable") + 1
assert Path(cmd[idx]).is_absolute()
assert "~" not in cmd[idx]
def test_with_requirements_resolves_dot(self):
"""Test that '.' in with_requirements becomes absolute."""
cmd = build_uv_command("server.py", with_requirements=Path("./reqs.txt"))
idx = cmd.index("--with-requirements") + 1
assert Path(cmd[idx]).is_absolute()
def test_with_requirements_resolves_tilde(self):
"""Test that '~' in with_requirements is expanded."""
cmd = build_uv_command("server.py", with_requirements=Path("~/reqs.txt"))
idx = cmd.index("--with-requirements") + 1
assert Path(cmd[idx]).is_absolute()
assert "~" not in cmd[idx]
def test_project_resolves_relative(self):
"""Test that relative path in project becomes absolute."""
cmd = build_uv_command("server.py", project=Path("../project"))
idx = cmd.index("--project") + 1
assert Path(cmd[idx]).is_absolute()
def test_project_resolves_tilde(self):
"""Test that '~' in project is expanded."""
cmd = build_uv_command("server.py", project=Path("~/work"))
idx = cmd.index("--project") + 1
assert Path(cmd[idx]).is_absolute()
assert "~" not in cmd[idx]