diff --git a/docs/public/schemas/fastmcp.json/latest.json b/docs/public/schemas/fastmcp.json/latest.json index 81d0ad754..c28a50452 100644 --- a/docs/public/schemas/fastmcp.json/latest.json +++ b/docs/public/schemas/fastmcp.json/latest.json @@ -243,17 +243,25 @@ "editable": { "anyOf": [ { - "type": "string" + "items": { + "type": "string" + }, + "type": "array" }, { "type": "null" } ], "default": null, - "description": "Directory to install in editable mode", + "description": "Directories to install in editable mode", "examples": [ - ".", - "../my-package" + [ + ".", + "../my-package" + ], + [ + "/path/to/package" + ] ], "title": "Editable" } diff --git a/docs/public/schemas/fastmcp.json/v1.json b/docs/public/schemas/fastmcp.json/v1.json index 81d0ad754..c28a50452 100644 --- a/docs/public/schemas/fastmcp.json/v1.json +++ b/docs/public/schemas/fastmcp.json/v1.json @@ -243,17 +243,25 @@ "editable": { "anyOf": [ { - "type": "string" + "items": { + "type": "string" + }, + "type": "array" }, { "type": "null" } ], "default": null, - "description": "Directory to install in editable mode", + "description": "Directories to install in editable mode", "examples": [ - ".", - "../my-package" + [ + ".", + "../my-package" + ], + [ + "/path/to/package" + ] ], "title": "Editable" } diff --git a/src/fastmcp/cli/claude.py b/src/fastmcp/cli/claude.py index fa4d529de..8873a3a7a 100644 --- a/src/fastmcp/cli/claude.py +++ b/src/fastmcp/cli/claude.py @@ -34,7 +34,7 @@ def update_claude_config( file_spec: str, server_name: str, *, - with_editable: Path | None = None, + with_editable: list[Path] | None = None, with_packages: list[str] | None = None, env_vars: dict[str, str] | None = None, ) -> bool: @@ -43,7 +43,7 @@ def update_claude_config( Args: file_spec: Path to the server file, optionally with :object suffix server_name: Name for the server in Claude's config - with_editable: Optional directory to install in editable mode + with_editable: Optional list of directories to install in editable mode with_packages: Optional list of additional packages to install env_vars: Optional dictionary of environment variables. These are merged with any existing variables, with new values taking precedence. @@ -101,7 +101,7 @@ def update_claude_config( # Build uv run command using Environment.build_uv_args() env_config = Environment( dependencies=deduplicated_packages, - editable=[str(with_editable)] if with_editable else None, + editable=[str(p) for p in with_editable] if with_editable else None, ) args = env_config.build_uv_args() diff --git a/src/fastmcp/cli/cli.py b/src/fastmcp/cli/cli.py index aba034e20..84611d0a3 100644 --- a/src/fastmcp/cli/cli.py +++ b/src/fastmcp/cli/cli.py @@ -126,20 +126,21 @@ async def dev( server_spec: str | None = None, *, with_editable: Annotated[ - Path | None, + list[Path] | None, cyclopts.Parameter( - name=["--with-editable", "-e"], - help="Directory containing pyproject.toml to install in editable mode", + "--with-editable", + help="Directory containing pyproject.toml to install in editable mode (can be used multiple times)", + negative="", ), ] = None, with_packages: Annotated[ - list[str], + list[str] | None, cyclopts.Parameter( "--with", - help="Additional packages to install", + help="Additional packages to install (can be used multiple times)", negative="", ), - ] = [], + ] = None, inspector_version: Annotated[ str | None, cyclopts.Parameter( @@ -188,6 +189,9 @@ async def dev( Args: server_spec: Python file to run, optionally with :object suffix, or None to auto-detect fastmcp.json """ + # Convert None to empty lists for list parameters + with_editable = with_editable or [] + with_packages = with_packages or [] from pathlib import Path from fastmcp.utilities.fastmcp_config import FastMCPConfig @@ -229,13 +233,9 @@ async def dev( if config.environment.requirements else None ) - # Note: config.environment.editable is a list, but CLI only supports single path - # Take the first editable path if available - with_editable = with_editable or ( - Path(config.environment.editable[0]) - if config.environment.editable and config.environment.editable[0] - else None - ) + # Merge editable paths from config with CLI args + if config.environment.editable and not with_editable: + with_editable = [Path(p) for p in config.environment.editable] # Merge packages from both sources if config.environment.dependencies: @@ -256,7 +256,7 @@ async def dev( "Starting dev server", extra={ "server_spec": server_spec, - "with_editable": str(with_editable) if with_editable else None, + "with_editable": [str(p) for p in with_editable] if with_editable else None, "with_packages": with_packages, "ui_port": ui_port, "server_port": server_port, @@ -305,7 +305,7 @@ async def dev( dependencies=with_packages if with_packages else None, requirements=str(with_requirements) if with_requirements else None, project=str(project) if project else None, - editable=[str(with_editable)] if with_editable else None, + editable=[str(p) for p in with_editable] if with_editable else None, ) uv_cmd = ["uv"] + env_config.build_uv_args(["fastmcp", "run", server_spec]) @@ -396,13 +396,13 @@ async def run( ), ] = None, with_packages: Annotated[ - list[str], + list[str] | None, cyclopts.Parameter( "--with", help="Additional packages to install (can be used multiple times)", negative="", ), - ] = [], + ] = None, project: Annotated[ Path | None, cyclopts.Parameter( @@ -450,6 +450,8 @@ async def run( Args: server_spec: Python file, object specification (file:obj), config file, URL, or None to auto-detect """ + # Convert None to empty lists for list parameters + with_packages = with_packages or [] # Load configuration if needed from pathlib import Path @@ -631,13 +633,13 @@ async def inspect( ), ] = None, with_packages: Annotated[ - list[str], + list[str] | None, cyclopts.Parameter( "--with", help="Additional packages to install (can be used multiple times)", negative="", ), - ] = [], + ] = None, project: Annotated[ Path | None, cyclopts.Parameter( @@ -670,6 +672,8 @@ async def inspect( Args: server_spec: Python file to inspect, optionally with :object suffix, or fastmcp.json """ + # Convert None to empty lists for list parameters + with_packages = with_packages or [] from pathlib import Path from fastmcp.utilities.fastmcp_config import FastMCPConfig diff --git a/src/fastmcp/cli/install/claude_code.py b/src/fastmcp/cli/install/claude_code.py index 472a34c54..aaa07503c 100644 --- a/src/fastmcp/cli/install/claude_code.py +++ b/src/fastmcp/cli/install/claude_code.py @@ -75,7 +75,7 @@ def install_claude_code( server_object: str | None, name: str, *, - with_editable: Path | None = None, + with_editable: list[Path] | None = None, with_packages: list[str] | None = None, env_vars: dict[str, str] | None = None, python_version: str | None = None, @@ -88,7 +88,7 @@ def install_claude_code( file: Path to the server file server_object: Optional server object name (for :object suffix) name: Name for the server in Claude Code - with_editable: Optional directory to install in editable mode + with_editable: Optional list of directories to install in editable mode with_packages: Optional list of additional packages to install env_vars: Optional dictionary of environment variables python_version: Optional Python version to use @@ -121,7 +121,7 @@ def install_claude_code( dependencies=deduplicated_packages, requirements=str(with_requirements) if with_requirements else None, project=str(project) if project else None, - editable=[str(with_editable)] if with_editable else None, + editable=[str(p) for p in with_editable] if with_editable else None, ) args = env_config.build_uv_args() @@ -171,28 +171,29 @@ async def claude_code_command( ), ] = None, with_editable: Annotated[ - Path | None, + list[Path] | None, cyclopts.Parameter( - name=["--with-editable", "-e"], - help="Directory with pyproject.toml to install in editable mode", + "--with-editable", + help="Directory with pyproject.toml to install in editable mode (can be used multiple times)", + negative="", ), ] = None, with_packages: Annotated[ - list[str], + list[str] | None, cyclopts.Parameter( "--with", - help="Additional packages to install", + help="Additional packages to install (can be used multiple times)", negative="", ), - ] = [], + ] = None, env_vars: Annotated[ - list[str], + list[str] | None, cyclopts.Parameter( "--env", - help="Environment variables in KEY=VALUE format", + help="Environment variables in KEY=VALUE format (can be used multiple times)", negative="", ), - ] = [], + ] = None, env_file: Annotated[ Path | None, cyclopts.Parameter( @@ -227,6 +228,10 @@ async def claude_code_command( Args: server_spec: Python file to install, optionally with :object suffix """ + # Convert None to empty lists for list parameters + with_editable = with_editable or [] + with_packages = with_packages or [] + env_vars = env_vars or [] file, server_object, name, packages, env_dict = await process_common_args( server_spec, server_name, with_packages, env_vars, env_file ) diff --git a/src/fastmcp/cli/install/claude_desktop.py b/src/fastmcp/cli/install/claude_desktop.py index 69e543415..28ce20eee 100644 --- a/src/fastmcp/cli/install/claude_desktop.py +++ b/src/fastmcp/cli/install/claude_desktop.py @@ -40,7 +40,7 @@ def install_claude_desktop( server_object: str | None, name: str, *, - with_editable: Path | None = None, + with_editable: list[Path] | None = None, with_packages: list[str] | None = None, env_vars: dict[str, str] | None = None, python_version: str | None = None, @@ -53,7 +53,7 @@ def install_claude_desktop( file: Path to the server file server_object: Optional server object name (for :object suffix) name: Name for the server in Claude's config - with_editable: Optional directory to install in editable mode + with_editable: Optional list of directories to install in editable mode with_packages: Optional list of additional packages to install env_vars: Optional dictionary of environment variables python_version: Optional Python version to use @@ -86,7 +86,7 @@ def install_claude_desktop( dependencies=deduplicated_packages, requirements=str(with_requirements) if with_requirements else None, project=str(project) if project else None, - editable=[str(with_editable)] if with_editable else None, + editable=[str(p) for p in with_editable] if with_editable else None, ) args = env_config.build_uv_args() @@ -143,28 +143,29 @@ async def claude_desktop_command( ), ] = None, with_editable: Annotated[ - Path | None, + list[Path] | None, cyclopts.Parameter( - name=["--with-editable", "-e"], - help="Directory with pyproject.toml to install in editable mode", + "--with-editable", + help="Directory with pyproject.toml to install in editable mode (can be used multiple times)", + negative="", ), ] = None, with_packages: Annotated[ - list[str], + list[str] | None, cyclopts.Parameter( "--with", - help="Additional packages to install", + help="Additional packages to install (can be used multiple times)", negative="", ), - ] = [], + ] = None, env_vars: Annotated[ - list[str], + list[str] | None, cyclopts.Parameter( "--env", - help="Environment variables in KEY=VALUE format", + help="Environment variables in KEY=VALUE format (can be used multiple times)", negative="", ), - ] = [], + ] = None, env_file: Annotated[ Path | None, cyclopts.Parameter( @@ -199,6 +200,10 @@ async def claude_desktop_command( Args: server_spec: Python file to install, optionally with :object suffix """ + # Convert None to empty lists for list parameters + with_editable = with_editable or [] + with_packages = with_packages or [] + env_vars = env_vars or [] file, server_object, name, with_packages, env_dict = await process_common_args( server_spec, server_name, with_packages, env_vars, env_file ) diff --git a/src/fastmcp/cli/install/cursor.py b/src/fastmcp/cli/install/cursor.py index 472dd610f..bbb94a872 100644 --- a/src/fastmcp/cli/install/cursor.py +++ b/src/fastmcp/cli/install/cursor.py @@ -71,7 +71,7 @@ def install_cursor_workspace( name: str, workspace_path: Path, *, - with_editable: Path | None = None, + with_editable: list[Path] | None = None, with_packages: list[str] | None = None, env_vars: dict[str, str] | None = None, python_version: str | None = None, @@ -85,7 +85,7 @@ def install_cursor_workspace( server_object: Optional server object name (for :object suffix) name: Name for the server in Cursor workspace_path: Path to the workspace directory - with_editable: Optional directory to install in editable mode + with_editable: Optional list of directories to install in editable mode with_packages: Optional list of additional packages to install env_vars: Optional dictionary of environment variables python_version: Optional Python version to use @@ -120,7 +120,7 @@ def install_cursor_workspace( dependencies=deduplicated_packages, requirements=str(with_requirements.resolve()) if with_requirements else None, project=str(project.resolve()) if project else None, - editable=[str(with_editable.resolve())] if with_editable else None, + editable=[str(p.resolve()) for p in with_editable] if with_editable else None, ) args = env_config.build_uv_args() @@ -161,7 +161,7 @@ def install_cursor( server_object: str | None, name: str, *, - with_editable: Path | None = None, + with_editable: list[Path] | None = None, with_packages: list[str] | None = None, env_vars: dict[str, str] | None = None, python_version: str | None = None, @@ -175,7 +175,7 @@ def install_cursor( file: Path to the server file server_object: Optional server object name (for :object suffix) name: Name for the server in Cursor - with_editable: Optional directory to install in editable mode + with_editable: Optional list of directories to install in editable mode with_packages: Optional list of additional packages to install env_vars: Optional dictionary of environment variables python_version: Optional Python version to use @@ -200,7 +200,7 @@ def install_cursor( dependencies=deduplicated_packages, requirements=str(with_requirements.resolve()) if with_requirements else None, project=str(project.resolve()) if project else None, - editable=[str(with_editable.resolve())] if with_editable else None, + editable=[str(p.resolve()) for p in with_editable] if with_editable else None, ) args = env_config.build_uv_args() @@ -262,28 +262,29 @@ async def cursor_command( ), ] = None, with_editable: Annotated[ - Path | None, + list[Path] | None, cyclopts.Parameter( - name=["--with-editable", "-e"], - help="Directory with pyproject.toml to install in editable mode", + "--with-editable", + help="Directory with pyproject.toml to install in editable mode (can be used multiple times)", + negative="", ), ] = None, with_packages: Annotated[ - list[str], + list[str] | None, cyclopts.Parameter( "--with", - help="Additional packages to install", + help="Additional packages to install (can be used multiple times)", negative="", ), - ] = [], + ] = None, env_vars: Annotated[ - list[str], + list[str] | None, cyclopts.Parameter( "--env", - help="Environment variables in KEY=VALUE format", + help="Environment variables in KEY=VALUE format (can be used multiple times)", negative="", ), - ] = [], + ] = None, env_file: Annotated[ Path | None, cyclopts.Parameter( @@ -325,6 +326,10 @@ async def cursor_command( Args: server_spec: Python file to install, optionally with :object suffix """ + # Convert None to empty lists for list parameters + with_editable = with_editable or [] + with_packages = with_packages or [] + env_vars = env_vars or [] file, server_object, name, with_packages, env_dict = await process_common_args( server_spec, server_name, with_packages, env_vars, env_file ) diff --git a/src/fastmcp/cli/install/mcp_json.py b/src/fastmcp/cli/install/mcp_json.py index 9315c40ef..42696ae6c 100644 --- a/src/fastmcp/cli/install/mcp_json.py +++ b/src/fastmcp/cli/install/mcp_json.py @@ -22,7 +22,7 @@ def install_mcp_json( server_object: str | None, name: str, *, - with_editable: Path | None = None, + with_editable: list[Path] | None = None, with_packages: list[str] | None = None, env_vars: dict[str, str] | None = None, copy: bool = False, @@ -36,7 +36,7 @@ def install_mcp_json( file: Path to the server file server_object: Optional server object name (for :object suffix) name: Name for the server in MCP config - with_editable: Optional directory to install in editable mode + with_editable: Optional list of directories to install in editable mode with_packages: Optional list of additional packages to install env_vars: Optional dictionary of environment variables copy: If True, copy to clipboard instead of printing to stdout @@ -61,7 +61,7 @@ def install_mcp_json( dependencies=deduplicated_packages, requirements=str(with_requirements) if with_requirements else None, project=str(project) if project else None, - editable=[str(with_editable)] if with_editable else None, + editable=[str(p) for p in with_editable] if with_editable else None, ) args = env_config.build_uv_args() @@ -116,28 +116,29 @@ async def mcp_json_command( ), ] = None, with_editable: Annotated[ - Path | None, + list[Path] | None, cyclopts.Parameter( - name=["--with-editable", "-e"], - help="Directory with pyproject.toml to install in editable mode", + "--with-editable", + help="Directory with pyproject.toml to install in editable mode (can be used multiple times)", + negative="", ), ] = None, with_packages: Annotated[ - list[str], + list[str] | None, cyclopts.Parameter( "--with", - help="Additional packages to install", + help="Additional packages to install (can be used multiple times)", negative="", ), - ] = [], + ] = None, env_vars: Annotated[ - list[str], + list[str] | None, cyclopts.Parameter( "--env", - help="Environment variables in KEY=VALUE format", + help="Environment variables in KEY=VALUE format (can be used multiple times)", negative="", ), - ] = [], + ] = None, env_file: Annotated[ Path | None, cyclopts.Parameter( @@ -180,6 +181,10 @@ async def mcp_json_command( Args: server_spec: Python file to install, optionally with :object suffix """ + # Convert None to empty lists for list parameters + with_editable = with_editable or [] + with_packages = with_packages or [] + env_vars = env_vars or [] file, server_object, name, packages, env_dict = await process_common_args( server_spec, server_name, with_packages, env_vars, env_file ) diff --git a/src/fastmcp/cli/install/shared.py b/src/fastmcp/cli/install/shared.py index 2f92fbd89..6db83403f 100644 --- a/src/fastmcp/cli/install/shared.py +++ b/src/fastmcp/cli/install/shared.py @@ -29,14 +29,17 @@ def parse_env_var(env_var: str) -> tuple[str, str]: async def process_common_args( server_spec: str, server_name: str | None, - with_packages: list[str], - env_vars: list[str], + with_packages: list[str] | None, + env_vars: list[str] | None, env_file: Path | None, ) -> tuple[Path, str | None, str, list[str], dict[str, str] | None]: """Process common arguments shared by all install commands. Handles both fastmcp.json config files and traditional file.py:object syntax. """ + # Convert None to empty lists for list parameters + with_packages = with_packages or [] + env_vars = env_vars or [] # Create FastMCPConfig from server_spec config = None if server_spec.endswith(".json"): diff --git a/tests/cli/test_cursor.py b/tests/cli/test_cursor.py index 008aff843..c5531bd8d 100644 --- a/tests/cli/test_cursor.py +++ b/tests/cli/test_cursor.py @@ -246,7 +246,7 @@ class TestInstallCursor: file=Path("/path/to/server.py"), server_object="custom_app", name="test-server", - with_editable=editable_path, + with_editable=[editable_path], ) assert result is True @@ -328,7 +328,7 @@ class TestCursorCommand: file=Path("server.py"), server_object=None, name="test-server", - with_editable=None, + with_editable=[], with_packages=[], env_vars={}, python_version=None,