diff --git a/docs/deployment/running-server.mdx b/docs/deployment/running-server.mdx index 6436c82da..5d98687ed 100644 --- a/docs/deployment/running-server.mdx +++ b/docs/deployment/running-server.mdx @@ -53,12 +53,48 @@ You can specify transport options and other configuration: fastmcp run server.py --transport sse --port 9000 ``` +### Dependency Management with CLI + +When using the FastMCP CLI, you can pass additional options to configure how `uv` runs your server: + +```bash +# Run with a specific Python version +fastmcp run server.py --python 3.11 + +# Run with additional packages +fastmcp run server.py --with pandas --with numpy + +# Run with dependencies from a requirements file +fastmcp run server.py --with-requirements requirements.txt + +# Combine multiple options +fastmcp run server.py --python 3.10 --with httpx --transport http + +# Run within a specific project directory +fastmcp run server.py --project /path/to/project +``` + + +When using `--python`, `--with`, `--project`, or `--with-requirements`, the server runs via `uv run` subprocess instead of using your local environment. The `uv` command will manage dependencies based on your project configuration. + + + +The `--python` option is particularly useful when you need to run a server with a specific Python version that differs from your system's default. This addresses common compatibility issues where servers require a particular Python version to function correctly. + + For development and testing, you can use the `dev` command to run your server with the MCP Inspector: ```bash fastmcp dev server.py ``` +The `dev` command also supports the same dependency management options: + +```bash +# Dev server with specific Python version and packages +fastmcp dev server.py --python 3.11 --with pandas +``` + See the [CLI documentation](/patterns/cli) for detailed information about all available commands and options. ### Passing Arguments to Servers diff --git a/docs/integrations/claude-code.mdx b/docs/integrations/claude-code.mdx index b17c63624..b8c21b87c 100644 --- a/docs/integrations/claude-code.mdx +++ b/docs/integrations/claude-code.mdx @@ -62,12 +62,26 @@ The command will automatically configure the server with Claude Code's `claude m #### Dependencies -If your server has dependencies, include them with the `--with` flag: +FastMCP provides flexible dependency management options for your Claude Code servers: + +**Individual packages**: Use the `--with` flag to specify packages your server needs. You can use this flag multiple times: ```bash fastmcp install claude-code server.py --with pandas --with requests ``` +**Requirements file**: If you maintain a `requirements.txt` file with all your dependencies, use `--with-requirements` to install them: + +```bash +fastmcp install claude-code server.py --with-requirements requirements.txt +``` + +**Editable packages**: For local packages under development, use `--with-editable` to install them in editable mode: + +```bash +fastmcp install claude-code server.py --with-editable ./my-local-package +``` + Alternatively, you can specify dependencies directly in your server code: ```python server.py @@ -79,14 +93,30 @@ mcp = FastMCP( ) ``` +#### Python Version and Project Configuration + +Control the Python environment for your server with these options: + +**Python version**: Use `--python` to specify which Python version your server requires. This ensures compatibility when your server needs specific Python features: + +```bash +fastmcp install claude-code server.py --python 3.11 +``` + +**Project directory**: Use `--project` to run your server within a specific project context. This tells `uv` to use the project's configuration files and virtual environment: + +```bash +fastmcp install claude-code server.py --project /path/to/my-project +``` + #### Environment Variables If your server needs environment variables (like API keys), you must include them: ```bash fastmcp install claude-code server.py --name "Weather Server" \ - --env-var API_KEY=your-api-key \ - --env-var DEBUG=true + --env API_KEY=your-api-key \ + --env DEBUG=true ``` Or load them from a `.env` file: @@ -101,7 +131,7 @@ fastmcp install claude-code server.py --name "Weather Server" --env-file .env ### Manual Configuration -For more control over the configuration, you can manually use Claude Code's built-in MCP management commands: +For more control over the configuration, you can manually use Claude Code's built-in MCP management commands. This gives you direct control over how your server is launched: ```bash # Add a server with custom configuration @@ -114,6 +144,16 @@ claude mcp add weather-server -e API_KEY=secret -e DEBUG=true -- uv run --with f claude mcp add my-server --scope user -- uv run --with fastmcp fastmcp run server.py ``` +You can also manually specify Python versions and project directories in your Claude Code commands: + +```bash +# With specific Python version +claude mcp add ml-server -- uv run --python 3.11 --with fastmcp fastmcp run server.py + +# Within a project directory +claude mcp add project-server -- uv run --project /path/to/project --with fastmcp fastmcp run server.py +``` + ## Using the Server Once your server is installed, you can start using your FastMCP server with Claude Code. diff --git a/docs/integrations/claude-desktop.mdx b/docs/integrations/claude-desktop.mdx index 2d37d523e..a69bebb0b 100644 --- a/docs/integrations/claude-desktop.mdx +++ b/docs/integrations/claude-desktop.mdx @@ -78,12 +78,26 @@ After installation, restart Claude Desktop completely. You should see a hammer i #### Dependencies -If your server has dependencies, include them with the `--with` flag: +FastMCP provides several ways to manage your server's dependencies when installing in Claude Desktop: + +**Individual packages**: Use the `--with` flag to specify packages your server needs. You can use this flag multiple times: ```bash fastmcp install claude-desktop server.py --with pandas --with requests ``` +**Requirements file**: If you have a `requirements.txt` file listing all your dependencies, use `--with-requirements` to install them all at once: + +```bash +fastmcp install claude-desktop server.py --with-requirements requirements.txt +``` + +**Editable packages**: For local packages in development, use `--with-editable` to install them in editable mode: + +```bash +fastmcp install claude-desktop server.py --with-editable ./my-local-package +``` + Alternatively, you can specify dependencies directly in your server code: ```python server.py @@ -95,6 +109,24 @@ mcp = FastMCP( ) ``` +#### Python Version and Project Directory + +FastMCP allows you to control the Python environment for your server: + +**Python version**: Use `--python` to specify which Python version your server should run with. This is particularly useful when your server requires a specific Python version: + +```bash +fastmcp install claude-desktop server.py --python 3.11 +``` + +**Project directory**: Use `--project` to run your server within a specific project directory. This ensures that `uv` will discover all `pyproject.toml`, `uv.toml`, and `.python-version` files from that project: + +```bash +fastmcp install claude-desktop server.py --project /path/to/my-project +``` + +When you specify a project directory, all relative paths in your server will be resolved from that directory, and the project's virtual environment will be used. + #### Environment Variables @@ -105,8 +137,8 @@ If your server needs environment variables (like API keys), you must include the ```bash fastmcp install claude-desktop server.py --name "Weather Server" \ - --env-var API_KEY=your-api-key \ - --env-var DEBUG=true + --env API_KEY=your-api-key \ + --env DEBUG=true ``` Or load them from a `.env` file: @@ -146,6 +178,8 @@ After updating the configuration file, restart Claude Desktop completely. Look f If your server has dependencies, you can use `uv` or another package manager to set up the environment. +When manually configuring dependencies, the recommended approach is to use `uv` with FastMCP. The configuration uses `uv run` to create an isolated environment with your specified packages: + ```json { "mcpServers": { @@ -153,9 +187,11 @@ If your server has dependencies, you can use `uv` or another package manager to "command": "uv", "args": [ "run", + "--with", "fastmcp", "--with", "pandas", "--with", "requests", - "python", + "fastmcp", + "run", "path/to/your/server.py" ] } @@ -163,6 +199,29 @@ If your server has dependencies, you can use `uv` or another package manager to } ``` +You can also manually specify Python versions and project directories in your configuration. Add `--python` to use a specific Python version, or `--project` to run within a project directory: + +```json +{ + "mcpServers": { + "dice-roller": { + "command": "uv", + "args": [ + "run", + "--python", "3.11", + "--project", "/path/to/project", + "--with", "fastmcp", + "fastmcp", + "run", + "path/to/your/server.py" + ] + } + } +} +``` + +The order of arguments matters: Python version and project settings come before package specifications, which come before the actual command to run. + - **`uv` must be installed and available in your system PATH**. Claude Desktop runs in its own isolated environment and needs `uv` to manage dependencies. - **On macOS, it is recommended to install `uv` globally with Homebrew** so that Claude Desktop will detect it: `brew install uv`. Installing `uv` with other methods may not make it accessible to Claude Desktop. diff --git a/docs/integrations/cursor.mdx b/docs/integrations/cursor.mdx index e44e770ba..0f0dec816 100644 --- a/docs/integrations/cursor.mdx +++ b/docs/integrations/cursor.mdx @@ -64,12 +64,26 @@ After running the command, Cursor will open automatically and prompt you to inst #### Dependencies -If your server has dependencies, include them with the `--with` flag: +FastMCP offers multiple ways to manage dependencies for your Cursor servers: + +**Individual packages**: Use the `--with` flag to specify packages your server needs. You can use this flag multiple times: ```bash fastmcp install cursor server.py --with pandas --with requests ``` +**Requirements file**: For projects with a `requirements.txt` file, use `--with-requirements` to install all dependencies at once: + +```bash +fastmcp install cursor server.py --with-requirements requirements.txt +``` + +**Editable packages**: When developing local packages, use `--with-editable` to install them in editable mode: + +```bash +fastmcp install cursor server.py --with-editable ./my-local-package +``` + Alternatively, you can specify dependencies directly in your server code: ```python server.py @@ -81,6 +95,22 @@ mcp = FastMCP( ) ``` +#### Python Version and Project Configuration + +Control your server's Python environment with these options: + +**Python version**: Use `--python` to specify which Python version your server should use. This is essential when your server requires specific Python features: + +```bash +fastmcp install cursor server.py --python 3.11 +``` + +**Project directory**: Use `--project` to run your server within a specific project context. This ensures `uv` discovers all project configuration files and uses the correct virtual environment: + +```bash +fastmcp install cursor server.py --project /path/to/my-project +``` + #### Environment Variables @@ -91,8 +121,8 @@ If your server needs environment variables (like API keys), you must include the ```bash fastmcp install cursor server.py --name "Weather Server" \ - --env-var API_KEY=your-api-key \ - --env-var DEBUG=true + --env API_KEY=your-api-key \ + --env DEBUG=true ``` Or load them from a `.env` file: @@ -147,6 +177,8 @@ After updating the configuration file, your server should be available in Cursor If your server has dependencies, you can use `uv` or another package manager to set up the environment. +When manually configuring dependencies, the recommended approach is to use `uv` with FastMCP. The configuration should use `uv run` to create an isolated environment with your specified packages: + ```json { "mcpServers": { @@ -154,9 +186,11 @@ If your server has dependencies, you can use `uv` or another package manager to "command": "uv", "args": [ "run", + "--with", "fastmcp", "--with", "pandas", "--with", "requests", - "python", + "fastmcp", + "run", "path/to/your/server.py" ] } @@ -164,6 +198,29 @@ If your server has dependencies, you can use `uv` or another package manager to } ``` +You can also manually specify Python versions and project directories in your configuration: + +```json +{ + "mcpServers": { + "dice-roller": { + "command": "uv", + "args": [ + "run", + "--python", "3.11", + "--project", "/path/to/project", + "--with", "fastmcp", + "fastmcp", + "run", + "path/to/your/server.py" + ] + } + } +} +``` + +Note that the order of arguments is important: Python version and project settings should come before package specifications. + **`uv` must be installed and available in your system PATH**. Cursor runs in its own isolated environment and needs `uv` to manage dependencies. diff --git a/docs/integrations/mcp-json-configuration.mdx b/docs/integrations/mcp-json-configuration.mdx index 9a185043c..f3be1db06 100644 --- a/docs/integrations/mcp-json-configuration.mdx +++ b/docs/integrations/mcp-json-configuration.mdx @@ -136,6 +136,10 @@ To use this in a client configuration file, add it to the `mcpServers` object in } ``` + +When using `--python`, `--project`, or `--with-requirements`, the generated configuration will include these options in the `uv run` command, ensuring your server runs with the correct Python version and dependencies. + + Different MCP clients may have specific configuration requirements or formatting needs. Always consult your client's documentation to ensure proper integration. @@ -165,6 +169,9 @@ fastmcp install mcp-json server.py --with pandas --with requests --with httpx # Editable local package fastmcp install mcp-json server.py --with-editable ./my-package + +# From requirements file +fastmcp install mcp-json server.py --with-requirements requirements.txt ``` You can also specify dependencies directly in your server code: @@ -190,6 +197,18 @@ fastmcp install mcp-json server.py \ fastmcp install mcp-json server.py --env-file .env ``` +### Python Version and Project Directory + +Specify Python version or run within a specific project: + +```bash +# Use specific Python version +fastmcp install mcp-json server.py --python 3.11 + +# Run within a project directory +fastmcp install mcp-json server.py --project /path/to/project +``` + ### Server Object Selection Use the same `file.py:object` notation as other FastMCP commands: @@ -250,6 +269,17 @@ fastmcp install mcp-json api_server.py \ --env TIMEOUT=30 ``` +### Advanced Configuration + +```bash +fastmcp install mcp-json ml_server.py \ + --name "ML Analysis Server" \ + --python 3.11 \ + --with-requirements requirements.txt \ + --project /home/user/ml-project \ + --env GPU_DEVICE=0 +``` + Output: ```json { @@ -275,6 +305,32 @@ Output: } ``` +The advanced configuration example generates: +```json +{ + "ML Analysis Server": { + "command": "uv", + "args": [ + "run", + "--python", + "3.11", + "--project", + "/home/user/ml-project", + "--with", + "fastmcp", + "--with-requirements", + "requirements.txt", + "fastmcp", + "run", + "/home/user/ml_server.py" + ], + "env": { + "GPU_DEVICE": "0" + } + } +} +``` + ### Pipeline Usage Save configuration to file: diff --git a/docs/patterns/cli.mdx b/docs/patterns/cli.mdx index dce1b5b32..e394b99f5 100644 --- a/docs/patterns/cli.mdx +++ b/docs/patterns/cli.mdx @@ -18,8 +18,8 @@ fastmcp --help | Command | Purpose | Dependency Management | | ------- | ------- | --------------------- | -| `run` | Run a FastMCP server directly | Uses your current environment; you are responsible for ensuring all dependencies are available | -| `dev` | Run a server with the MCP Inspector for testing | Creates an isolated environment; dependencies must be explicitly specified with `--with` and/or `--with-editable` | +| `run` | Run a FastMCP server directly | Default: Uses your local environment directly. With `--python`, `--with`, `--project`, or `--with-requirements`: Runs via `uv run` subprocess | +| `dev` | Run a server with the MCP Inspector for testing | Always runs via `uv run` subprocess (never uses your local environment); dependencies must be specified or available in a uv-managed project | | `install` | Install a server in MCP client applications | Creates an isolated environment; dependencies must be explicitly specified with `--with` and/or `--with-editable` | | `inspect` | Generate a JSON report about a FastMCP server | Uses your current environment; you are responsible for ensuring all dependencies are available | | `version` | Display version information | N/A | @@ -35,7 +35,7 @@ fastmcp run server.py ``` -This command runs the server directly in your current Python environment. You are responsible for ensuring all dependencies are available. +By default, this command runs the server directly in your current Python environment. You are responsible for ensuring all dependencies are available. When using `--python`, `--with`, `--project`, or `--with-requirements` options, it runs the server via `uv run` subprocess instead. #### Options @@ -48,6 +48,10 @@ This command runs the server directly in your current Python environment. You ar | Path | `--path` | Path to bind to when using http transport (default: `/mcp/` or `/sse/` for SSE) | | Log Level | `--log-level`, `-l` | Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL) | | No Banner | `--no-banner` | Disable the startup banner display | +| Python Version | `--python` | Python version to use (e.g., 3.10, 3.11) | +| Additional Packages | `--with` | Additional packages to install (can be used multiple times) | +| Project Directory | `--project` | Run the command within the given project directory | +| Requirements File | `--with-requirements` | Requirements file to install dependencies from | #### Server Specification @@ -96,6 +100,18 @@ fastmcp run https://example.com/mcp-server # Connect to a remote server with specified log level fastmcp run https://example.com/mcp-server --log-level DEBUG + +# Run with a specific Python version +fastmcp run server.py --python 3.11 + +# Run with additional packages +fastmcp run server.py --with pandas --with numpy + +# Run within a specific project directory +fastmcp run server.py --project /path/to/project + +# Run with dependencies from a requirements file +fastmcp run server.py --with-requirements requirements.txt ``` ### `dev` @@ -107,7 +123,7 @@ fastmcp dev server.py ``` -This command runs your server in an isolated environment. All dependencies must be explicitly specified using the `--with` and/or `--with-editable` options. +This command always runs your server via `uv run` subprocess (never your local environment) to work with the MCP Inspector. All dependencies must be explicitly specified using the `--with` and/or `--with-editable` options, or be available in a uv-managed project. @@ -136,12 +152,24 @@ This command does not support HTTP testing. To test a server over Streamable HTT | Inspector Version | `--inspector-version` | Version of the MCP Inspector to use | | UI Port | `--ui-port` | Port for the MCP Inspector UI | | Server Port | `--server-port` | Port for the MCP Inspector Proxy server | +| Python Version | `--python` | Python version to use (e.g., 3.10, 3.11) | +| Project Directory | `--project` | Run the command within the given project directory | +| Requirements File | `--with-requirements` | Requirements file to install dependencies from | -**Example** +**Examples** ```bash # Run dev server with editable mode and additional packages fastmcp dev server.py -e . --with pandas --with matplotlib + +# Run dev server with specific Python version +fastmcp dev server.py --python 3.11 + +# Run dev server with requirements file +fastmcp dev server.py --with-requirements requirements.txt + +# Run dev server within a specific project directory +fastmcp dev server.py --project /path/to/project ``` ### `install` @@ -167,6 +195,10 @@ Note that for security reasons, MCP clients usually run every server in a comple **`uv` must be installed and available in your system PATH**. Both Claude Desktop and Cursor run in isolated environments and need `uv` to manage dependencies. On macOS, install `uv` globally with Homebrew for Claude Desktop compatibility: `brew install uv`. + +**Python Version Considerations**: The install commands now support the `--python` option to specify a Python version directly. You can also use `--project` to run within a specific project directory or `--with-requirements` to install dependencies from a requirements file. + + **FastMCP `install` commands focus on local server files with STDIO transport.** For remote servers running with HTTP or SSE transport, use your client's native configuration - FastMCP's value is simplifying the complex local setup with dependencies and `uv` commands. @@ -187,6 +219,9 @@ The `install` command supports the same `file.py:object` notation as the `run` c | Additional Packages | `--with` | Additional packages to install (can be used multiple times) | | Environment Variables | `--env` | Environment variables in KEY=VALUE format (can be used multiple times) | | Environment File | `--env-file`, `-f` | Load environment variables from a .env file | +| Python Version | `--python` | Python version to use (e.g., 3.10, 3.11) | +| Project Directory | `--project` | Run the command within the given project directory | +| Requirements File | `--with-requirements` | Requirements file to install dependencies from | **Examples** @@ -209,6 +244,15 @@ fastmcp install cursor server.py --env API_KEY=secret --env DEBUG=true # Install with environment file fastmcp install cursor server.py --env-file .env +# Install with specific Python version +fastmcp install claude-desktop server.py --python 3.11 + +# Install with requirements file +fastmcp install claude-code server.py --with-requirements requirements.txt + +# Install within a project directory +fastmcp install cursor server.py --project /path/to/project + # Generate MCP JSON configuration fastmcp install mcp-json server.py --name "My Server" --with pandas diff --git a/src/fastmcp/cli/cli.py b/src/fastmcp/cli/cli.py index a7ecf771f..1d0a45973 100644 --- a/src/fastmcp/cli/cli.py +++ b/src/fastmcp/cli/cli.py @@ -62,11 +62,22 @@ def _build_uv_command( 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"] + cmd = ["uv", "run"] - cmd.extend(["run", "--with", "fastmcp"]) + # 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)]) @@ -76,6 +87,9 @@ def _build_uv_command( 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]) @@ -163,6 +177,27 @@ def dev( help="Port for the MCP Inspector Proxy server", ), ] = None, + python: Annotated[ + str | None, + cyclopts.Parameter( + "--python", + help="Python version to use (e.g., 3.10, 3.11)", + ), + ] = None, + with_requirements: Annotated[ + Path | None, + cyclopts.Parameter( + "--with-requirements", + help="Requirements file to install dependencies from", + ), + ] = None, + project: Annotated[ + Path | None, + cyclopts.Parameter( + "--project", + help="Run the command within the given project directory", + ), + ] = None, ) -> None: """Run an MCP server with the MCP Inspector for development. @@ -209,7 +244,13 @@ def dev( inspector_cmd += f"@{inspector_version}" uv_cmd = _build_uv_command( - server_spec, with_editable, with_packages, no_banner=True + server_spec, + with_editable, + with_packages, + no_banner=True, + python_version=python, + with_requirements=with_requirements, + project=project, ) # Run the MCP Inspector command with shell=True on Windows @@ -288,6 +329,35 @@ def run( negative=False, ), ] = False, + python: Annotated[ + str | None, + cyclopts.Parameter( + "--python", + help="Python version to use (e.g., 3.10, 3.11)", + ), + ] = None, + with_packages: Annotated[ + list[str], + cyclopts.Parameter( + "--with", + help="Additional packages to install (can be used multiple times)", + negative=False, + ), + ] = [], + project: Annotated[ + Path | None, + cyclopts.Parameter( + "--project", + help="Run the command within the given project directory", + ), + ] = None, + with_requirements: Annotated[ + Path | None, + cyclopts.Parameter( + "--with-requirements", + help="Requirements file to install dependencies from", + ), + ] = None, ) -> None: """Run an MCP server or connect to a remote one. @@ -318,26 +388,53 @@ def run( }, ) - try: - run_module.run_command( - server_spec=server_spec, - transport=transport, - host=host, - port=port, - path=path, - log_level=log_level, - server_args=server_args, - show_banner=not no_banner, - ) - except Exception as e: - logger.error( - f"Failed to run: {e}", - extra={ - "server_spec": server_spec, - "error": str(e), - }, - ) - sys.exit(1) + # If any uv-specific options are provided, use uv run + if python or with_packages or with_requirements or project: + try: + run_module.run_with_uv( + server_spec=server_spec, + python_version=python, + with_packages=with_packages, + with_requirements=with_requirements, + project=project, + transport=transport, + host=host, + port=port, + path=path, + log_level=log_level, + show_banner=not no_banner, + ) + except Exception as e: + logger.error( + f"Failed to run: {e}", + extra={ + "server_spec": server_spec, + "error": str(e), + }, + ) + sys.exit(1) + else: + # Use direct import for backwards compatibility + try: + run_module.run_command( + server_spec=server_spec, + transport=transport, + host=host, + port=port, + path=path, + log_level=log_level, + server_args=server_args, + show_banner=not no_banner, + ) + except Exception as e: + logger.error( + f"Failed to run: {e}", + extra={ + "server_spec": server_spec, + "error": str(e), + }, + ) + sys.exit(1) @app.command diff --git a/src/fastmcp/cli/install/claude_code.py b/src/fastmcp/cli/install/claude_code.py index 59923e9ad..7f8e89d72 100644 --- a/src/fastmcp/cli/install/claude_code.py +++ b/src/fastmcp/cli/install/claude_code.py @@ -77,6 +77,9 @@ def install_claude_code( with_editable: Path | None = None, with_packages: list[str] | None = None, env_vars: dict[str, str] | None = None, + python_version: str | None = None, + with_requirements: Path | None = None, + project: Path | None = None, ) -> bool: """Install FastMCP server in Claude Code. @@ -87,6 +90,9 @@ def install_claude_code( with_editable: Optional directory 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 + with_requirements: Optional requirements file to install from + project: Optional project directory to run within Returns: True if installation was successful, False otherwise @@ -103,6 +109,14 @@ def install_claude_code( # 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: @@ -115,6 +129,9 @@ def install_claude_code( if with_editable: args.extend(["--with-editable", str(with_editable)]) + if with_requirements: + args.extend(["--with-requirements", str(with_requirements)]) + # Build server spec from parsed components if server_object: server_spec = f"{file.resolve()}:{server_object}" @@ -190,6 +207,27 @@ def claude_code_command( help="Load environment variables from .env file", ), ] = None, + python: Annotated[ + str | None, + cyclopts.Parameter( + "--python", + help="Python version to use (e.g., 3.10, 3.11)", + ), + ] = None, + with_requirements: Annotated[ + Path | None, + cyclopts.Parameter( + "--with-requirements", + help="Requirements file to install dependencies from", + ), + ] = None, + project: Annotated[ + Path | None, + cyclopts.Parameter( + "--project", + help="Run the command within the given project directory", + ), + ] = None, ) -> None: """Install an MCP server in Claude Code. @@ -207,6 +245,9 @@ def claude_code_command( with_editable=with_editable, with_packages=packages, env_vars=env_dict, + python_version=python, + with_requirements=with_requirements, + project=project, ) if success: diff --git a/src/fastmcp/cli/install/claude_desktop.py b/src/fastmcp/cli/install/claude_desktop.py index f23ecbd11..c47426de9 100644 --- a/src/fastmcp/cli/install/claude_desktop.py +++ b/src/fastmcp/cli/install/claude_desktop.py @@ -42,6 +42,9 @@ def install_claude_desktop( with_editable: Path | None = None, with_packages: list[str] | None = None, env_vars: dict[str, str] | None = None, + python_version: str | None = None, + with_requirements: Path | None = None, + project: Path | None = None, ) -> bool: """Install FastMCP server in Claude Desktop. @@ -52,6 +55,9 @@ def install_claude_desktop( with_editable: Optional directory 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 + with_requirements: Optional requirements file to install from + project: Optional project directory to run within Returns: True if installation was successful, False otherwise @@ -69,6 +75,14 @@ def install_claude_desktop( # 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: @@ -81,6 +95,9 @@ def install_claude_desktop( if with_editable: args.extend(["--with-editable", str(with_editable)]) + if with_requirements: + args.extend(["--with-requirements", str(with_requirements)]) + # Build server spec from parsed components if server_object: server_spec = f"{file.resolve()}:{server_object}" @@ -163,6 +180,27 @@ def claude_desktop_command( help="Load environment variables from .env file", ), ] = None, + python: Annotated[ + str | None, + cyclopts.Parameter( + "--python", + help="Python version to use (e.g., 3.10, 3.11)", + ), + ] = None, + with_requirements: Annotated[ + Path | None, + cyclopts.Parameter( + "--with-requirements", + help="Requirements file to install dependencies from", + ), + ] = None, + project: Annotated[ + Path | None, + cyclopts.Parameter( + "--project", + help="Run the command within the given project directory", + ), + ] = None, ) -> None: """Install an MCP server in Claude Desktop. @@ -180,6 +218,9 @@ def claude_desktop_command( with_editable=with_editable, with_packages=with_packages, env_vars=env_dict, + python_version=python, + with_requirements=with_requirements, + project=project, ) if not success: diff --git a/src/fastmcp/cli/install/cursor.py b/src/fastmcp/cli/install/cursor.py index 6bdd49c65..23e611687 100644 --- a/src/fastmcp/cli/install/cursor.py +++ b/src/fastmcp/cli/install/cursor.py @@ -72,6 +72,9 @@ def install_cursor( with_editable: Path | None = None, with_packages: list[str] | None = None, env_vars: dict[str, str] | None = None, + python_version: str | None = None, + with_requirements: Path | None = None, + project: Path | None = None, ) -> bool: """Install FastMCP server in Cursor. @@ -82,6 +85,9 @@ def install_cursor( with_editable: Optional directory 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 + with_requirements: Optional requirements file to install from + project: Optional project directory to run within Returns: True if installation was successful, False otherwise @@ -89,6 +95,14 @@ def install_cursor( # 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: @@ -101,6 +115,9 @@ def install_cursor( if with_editable: args.extend(["--with-editable", str(with_editable)]) + if with_requirements: + args.extend(["--with-requirements", str(with_requirements)]) + # Build server spec from parsed components if server_object: server_spec = f"{file.resolve()}:{server_object}" @@ -173,6 +190,27 @@ def cursor_command( help="Load environment variables from .env file", ), ] = None, + python: Annotated[ + str | None, + cyclopts.Parameter( + "--python", + help="Python version to use (e.g., 3.10, 3.11)", + ), + ] = None, + with_requirements: Annotated[ + Path | None, + cyclopts.Parameter( + "--with-requirements", + help="Requirements file to install dependencies from", + ), + ] = None, + project: Annotated[ + Path | None, + cyclopts.Parameter( + "--project", + help="Run the command within the given project directory", + ), + ] = None, ) -> None: """Install an MCP server in Cursor. @@ -190,6 +228,9 @@ def cursor_command( with_editable=with_editable, with_packages=with_packages, env_vars=env_dict, + python_version=python, + with_requirements=with_requirements, + project=project, ) if not success: diff --git a/src/fastmcp/cli/install/mcp_json.py b/src/fastmcp/cli/install/mcp_json.py index 91bd40ece..1afaa7a3b 100644 --- a/src/fastmcp/cli/install/mcp_json.py +++ b/src/fastmcp/cli/install/mcp_json.py @@ -25,6 +25,9 @@ def install_mcp_json( with_packages: list[str] | None = None, env_vars: dict[str, str] | None = None, copy: bool = False, + python_version: str | None = None, + with_requirements: Path | None = None, + project: Path | None = None, ) -> bool: """Generate MCP configuration JSON for manual installation. @@ -36,6 +39,9 @@ def install_mcp_json( 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 + python_version: Optional Python version to use + with_requirements: Optional requirements file to install from + project: Optional project directory to run within Returns: True if generation was successful, False otherwise @@ -44,6 +50,14 @@ def install_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: @@ -56,6 +70,9 @@ def install_mcp_json( if with_editable: args.extend(["--with-editable", str(with_editable)]) + if with_requirements: + args.extend(["--with-requirements", str(with_requirements)]) + # Build server spec from parsed components if server_object: server_spec = f"{file.resolve()}:{server_object}" @@ -144,6 +161,27 @@ def mcp_json_command( negative=False, ), ] = False, + python: Annotated[ + str | None, + cyclopts.Parameter( + "--python", + help="Python version to use (e.g., 3.10, 3.11)", + ), + ] = None, + with_requirements: Annotated[ + Path | None, + cyclopts.Parameter( + "--with-requirements", + help="Requirements file to install dependencies from", + ), + ] = None, + project: Annotated[ + Path | None, + cyclopts.Parameter( + "--project", + help="Run the command within the given project directory", + ), + ] = None, ) -> None: """Generate MCP configuration JSON for manual installation. @@ -162,6 +200,9 @@ def mcp_json_command( with_packages=packages, env_vars=env_dict, copy=copy, + python_version=python, + with_requirements=with_requirements, + project=project, ) if not success: diff --git a/src/fastmcp/cli/run.py b/src/fastmcp/cli/run.py index 373d9cfbb..f0464d2e4 100644 --- a/src/fastmcp/cli/run.py +++ b/src/fastmcp/cli/run.py @@ -2,6 +2,7 @@ import importlib.util import re +import subprocess import sys from pathlib import Path from typing import Any, Literal @@ -122,6 +123,84 @@ def import_server(file: Path, server_object: str | None = None) -> Any: return server +def run_with_uv( + server_spec: str, + python_version: str | None = None, + with_packages: list[str] | None = None, + with_requirements: Path | None = None, + project: Path | None = None, + transport: TransportType | None = None, + host: str | None = None, + port: int | None = None, + path: str | None = None, + log_level: LogLevelType | None = None, + show_banner: bool = True, +) -> None: + """Run a MCP server using uv run subprocess. + + Args: + server_spec: Python file, object specification (file:obj), or URL + python_version: Python version to use (e.g. "3.10") + with_packages: Additional packages to install + with_requirements: Requirements file to use + project: Run the command within the given project directory + transport: Transport protocol to use + host: Host to bind to when using http transport + port: Port to bind to when using http transport + path: Path to bind to when using http transport + log_level: Log level + show_banner: Whether to show the server banner + """ + 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]) + + # Add transport options + if transport: + cmd.extend(["--transport", transport]) + if host: + cmd.extend(["--host", host]) + if port: + cmd.extend(["--port", str(port)]) + if path: + cmd.extend(["--path", path]) + if log_level: + cmd.extend(["--log-level", log_level]) + if not show_banner: + cmd.append("--no-banner") + + # Run the command + logger.debug(f"Running command: {' '.join(cmd)}") + try: + process = subprocess.run(cmd, check=True) + sys.exit(process.returncode) + except subprocess.CalledProcessError as e: + logger.error(f"Failed to run server: {e}") + sys.exit(e.returncode) + + def create_client_server(url: str) -> Any: """Create a FastMCP server from a client URL. @@ -175,6 +254,7 @@ def run_command( log_level: LogLevelType | None = None, server_args: list[str] | None = None, show_banner: bool = True, + use_direct_import: bool = False, ) -> None: """Run a MCP server or connect to a remote one. @@ -187,6 +267,7 @@ def run_command( log_level: Log level server_args: Additional arguments to pass to the server show_banner: Whether to show the server banner + use_direct_import: Whether to use direct import instead of subprocess """ if is_url(server_spec): # Handle URL case diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index 097c7b17e..13b304446 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -90,6 +90,94 @@ class TestMainCLI: ] 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.""" @@ -167,6 +255,29 @@ class TestDevCommand: assert bound.arguments["inspector_version"] == "1.0.0" assert bound.arguments["ui_port"] == 3000 + def test_dev_command_parsing_with_new_options(self): + """Test dev command parsing with new uv options.""" + command, bound, _ = app.parse_args( + [ + "dev", + "server.py", + "--python", + "3.10", + "--project", + "/workspace", + "--with-requirements", + "dev-requirements.txt", + "--with", + "pytest", + ] + ) + assert command is not None + assert bound.arguments["server_spec"] == "server.py" + assert bound.arguments["python"] == "3.10" + assert bound.arguments["project"] == Path("/workspace") + assert bound.arguments["with_requirements"] == Path("dev-requirements.txt") + assert bound.arguments["with_packages"] == ["pytest"] + class TestRunCommand: """Test the run command.""" @@ -236,6 +347,32 @@ class TestRunCommand: assert "log_level" not in bound.arguments assert "path" not in bound.arguments + def test_run_command_parsing_with_new_options(self): + """Test run command parsing with new uv options.""" + command, bound, _ = app.parse_args( + [ + "run", + "server.py", + "--python", + "3.11", + "--with", + "pandas", + "--with", + "numpy", + "--project", + "/path/to/project", + "--with-requirements", + "requirements.txt", + ] + ) + + assert command is not None + assert bound.arguments["server_spec"] == "server.py" + assert bound.arguments["python"] == "3.11" + assert bound.arguments["with_packages"] == ["pandas", "numpy"] + assert bound.arguments["project"] == Path("/path/to/project") + assert bound.arguments["with_requirements"] == Path("requirements.txt") + def test_run_command_transport_aliases(self): """Test that both 'http' and 'streamable-http' are accepted as valid transport options.""" # Test with 'http' transport diff --git a/tests/cli/test_cursor.py b/tests/cli/test_cursor.py index 5f3820b9d..2383f211a 100644 --- a/tests/cli/test_cursor.py +++ b/tests/cli/test_cursor.py @@ -327,6 +327,9 @@ class TestCursorCommand: with_editable=None, with_packages=[], env_vars={}, + python_version=None, + with_requirements=None, + project=None, ) mock_exit.assert_not_called() diff --git a/tests/cli/test_install.py b/tests/cli/test_install.py index a16a1e61d..cefa541dc 100644 --- a/tests/cli/test_install.py +++ b/tests/cli/test_install.py @@ -1,3 +1,5 @@ +from pathlib import Path + from fastmcp.cli.install import install_app @@ -63,6 +65,27 @@ class TestClaudeCodeInstall: assert bound.arguments["with_packages"] == ["package1", "package2"] assert bound.arguments["env_vars"] == ["VAR1=value1"] + def test_claude_code_with_new_options(self): + """Test claude-code install with new uv options.""" + from pathlib import Path + + command, bound, _ = install_app.parse_args( + [ + "claude-code", + "server.py", + "--python", + "3.11", + "--project", + "/workspace", + "--with-requirements", + "requirements.txt", + ] + ) + + assert bound.arguments["python"] == "3.11" + assert bound.arguments["project"] == Path("/workspace") + assert bound.arguments["with_requirements"] == Path("requirements.txt") + class TestClaudeDesktopInstall: """Test claude-desktop install command.""" @@ -94,6 +117,27 @@ class TestClaudeDesktopInstall: assert bound.arguments["env_vars"] == ["VAR1=value1", "VAR2=value2"] + def test_claude_desktop_with_new_options(self): + """Test claude-desktop install with new uv options.""" + from pathlib import Path + + command, bound, _ = install_app.parse_args( + [ + "claude-desktop", + "server.py", + "--python", + "3.10", + "--project", + "/my/project", + "--with-requirements", + "reqs.txt", + ] + ) + + assert bound.arguments["python"] == "3.10" + assert bound.arguments["project"] == Path("/my/project") + assert bound.arguments["with_requirements"] == Path("reqs.txt") + class TestCursorInstall: """Test cursor install command.""" @@ -163,3 +207,45 @@ class TestInstallCommandParsing: command, bound, _ = install_app.parse_args(["mcp-json", "server.py"]) assert command is not None assert bound.arguments["server_spec"] == "server.py" + + def test_python_option(self): + """Test --python option for all install commands.""" + commands_to_test = [ + ["claude-code", "server.py", "--python", "3.11"], + ["claude-desktop", "server.py", "--python", "3.11"], + ["cursor", "server.py", "--python", "3.11"], + ["mcp-json", "server.py", "--python", "3.11"], + ] + + for cmd_args in commands_to_test: + command, bound, _ = install_app.parse_args(cmd_args) + assert command is not None + assert bound.arguments["python"] == "3.11" + + def test_with_requirements_option(self): + """Test --with-requirements option for all install commands.""" + commands_to_test = [ + ["claude-code", "server.py", "--with-requirements", "requirements.txt"], + ["claude-desktop", "server.py", "--with-requirements", "requirements.txt"], + ["cursor", "server.py", "--with-requirements", "requirements.txt"], + ["mcp-json", "server.py", "--with-requirements", "requirements.txt"], + ] + + for cmd_args in commands_to_test: + command, bound, _ = install_app.parse_args(cmd_args) + assert command is not None + assert str(bound.arguments["with_requirements"]) == "requirements.txt" + + def test_project_option(self): + """Test --project option for all install commands.""" + commands_to_test = [ + ["claude-code", "server.py", "--project", "/path/to/project"], + ["claude-desktop", "server.py", "--project", "/path/to/project"], + ["cursor", "server.py", "--project", "/path/to/project"], + ["mcp-json", "server.py", "--project", "/path/to/project"], + ] + + for cmd_args in commands_to_test: + command, bound, _ = install_app.parse_args(cmd_args) + assert command is not None + assert str(bound.arguments["project"]) == str(Path("/path/to/project")) diff --git a/tests/cli/test_run_with_uv.py b/tests/cli/test_run_with_uv.py new file mode 100644 index 000000000..3aac81fa7 --- /dev/null +++ b/tests/cli/test_run_with_uv.py @@ -0,0 +1,240 @@ +"""Tests for the run_with_uv function and related functionality.""" + +import subprocess +from pathlib import Path +from unittest.mock import Mock, patch + +import pytest + +from fastmcp.cli.run import run_with_uv + + +class TestRunWithUv: + """Test the run_with_uv function.""" + + @patch("subprocess.run") + def test_run_with_uv_basic(self, mock_run): + """Test basic run_with_uv execution.""" + mock_run.return_value = Mock(returncode=0) + + with pytest.raises(SystemExit) as exc_info: + run_with_uv("server.py") + + assert exc_info.value.code == 0 + + # Check the command that was called + mock_run.assert_called_once() + cmd = mock_run.call_args[0][0] + + expected = ["uv", "run", "--with", "fastmcp", "fastmcp", "run", "server.py"] + assert cmd == expected + + @patch("subprocess.run") + def test_run_with_uv_python_version(self, mock_run): + """Test run_with_uv with Python version.""" + mock_run.return_value = Mock(returncode=0) + + with pytest.raises(SystemExit) as exc_info: + run_with_uv("server.py", python_version="3.11") + + assert exc_info.value.code == 0 + + cmd = mock_run.call_args[0][0] + expected = [ + "uv", + "run", + "--python", + "3.11", + "--with", + "fastmcp", + "fastmcp", + "run", + "server.py", + ] + assert cmd == expected + + @patch("subprocess.run") + def test_run_with_uv_project(self, mock_run): + """Test run_with_uv with project directory.""" + mock_run.return_value = Mock(returncode=0) + project_path = Path("/my/project") + + with pytest.raises(SystemExit) as exc_info: + run_with_uv("server.py", project=project_path) + + 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 + + @patch("subprocess.run") + def test_run_with_uv_with_packages(self, mock_run): + """Test run_with_uv with additional packages.""" + mock_run.return_value = Mock(returncode=0) + + with pytest.raises(SystemExit) as exc_info: + run_with_uv("server.py", with_packages=["pandas", "numpy"]) + + assert exc_info.value.code == 0 + + cmd = mock_run.call_args[0][0] + expected = [ + "uv", + "run", + "--with", + "fastmcp", + "--with", + "pandas", + "--with", + "numpy", + "fastmcp", + "run", + "server.py", + ] + assert cmd == expected + + @patch("subprocess.run") + def test_run_with_uv_with_requirements(self, mock_run): + """Test run_with_uv with requirements file.""" + mock_run.return_value = Mock(returncode=0) + req_path = Path("requirements.txt") + + with pytest.raises(SystemExit) as exc_info: + run_with_uv("server.py", with_requirements=req_path) + + assert exc_info.value.code == 0 + + cmd = mock_run.call_args[0][0] + expected = [ + "uv", + "run", + "--with", + "fastmcp", + "--with-requirements", + "requirements.txt", + "fastmcp", + "run", + "server.py", + ] + assert cmd == expected + + @patch("subprocess.run") + def test_run_with_uv_transport_options(self, mock_run): + """Test run_with_uv with transport-related options.""" + mock_run.return_value = Mock(returncode=0) + + with pytest.raises(SystemExit) as exc_info: + run_with_uv( + "server.py", + transport="http", + host="localhost", + port=8080, + path="/api", + log_level="DEBUG", + show_banner=False, + ) + + assert exc_info.value.code == 0 + + cmd = mock_run.call_args[0][0] + expected = [ + "uv", + "run", + "--with", + "fastmcp", + "fastmcp", + "run", + "server.py", + "--transport", + "http", + "--host", + "localhost", + "--port", + "8080", + "--path", + "/api", + "--log-level", + "DEBUG", + "--no-banner", + ] + assert cmd == expected + + @patch("subprocess.run") + def test_run_with_uv_all_options(self, mock_run): + """Test run_with_uv with all options combined.""" + mock_run.return_value = Mock(returncode=0) + + with pytest.raises(SystemExit) as exc_info: + run_with_uv( + "server.py", + python_version="3.10", + project=Path("/workspace"), + with_packages=["pandas"], + with_requirements=Path("reqs.txt"), + transport="http", + port=9000, + show_banner=False, + ) + + 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", + "fastmcp", + "run", + "server.py", + "--transport", + "http", + "--port", + "9000", + "--no-banner", + ] + assert cmd == expected + + @patch("subprocess.run") + def test_run_with_uv_error_handling(self, mock_run): + """Test run_with_uv error handling.""" + mock_run.side_effect = subprocess.CalledProcessError(1, ["uv", "run"]) + + with pytest.raises(SystemExit) as exc_info: + run_with_uv("server.py") + + assert exc_info.value.code == 1 + + @patch("fastmcp.cli.run.logger") + @patch("subprocess.run") + def test_run_with_uv_logging(self, mock_run, mock_logger): + """Test that run_with_uv logs the command.""" + mock_run.return_value = Mock(returncode=0) + + with pytest.raises(SystemExit): + run_with_uv("server.py", python_version="3.11") + + # Check that debug logging was called with the command + mock_logger.debug.assert_called() + call_args = mock_logger.debug.call_args[0][0] + assert "Running command:" in call_args + assert "uv run --python 3.11" in call_args