Merge branch 'patch-1' of github.com:vl-kp/fastmcp into patch-1

This commit is contained in:
vincent 2025-08-27 08:38:30 +08:00
commit a73e798b36
10 changed files with 128 additions and 85 deletions

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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()

View file

@ -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

View file

@ -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
)

View file

@ -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
)

View file

@ -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
)

View file

@ -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
)

View file

@ -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"):

View file

@ -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,