From d52d6d8fffb49db54846a97aebcdb18845169eef Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 6 Aug 2025 13:12:52 -0400 Subject: [PATCH] Support factory functions in fastmcp run (#1384) --- docs/patterns/cli.mdx | 250 +++++++++++++--------- src/fastmcp/cli/cli.py | 10 +- src/fastmcp/cli/install/claude_code.py | 4 +- src/fastmcp/cli/install/claude_desktop.py | 4 +- src/fastmcp/cli/install/cursor.py | 4 +- src/fastmcp/cli/install/mcp_json.py | 4 +- src/fastmcp/cli/install/shared.py | 4 +- src/fastmcp/cli/run.py | 98 ++++++--- tests/cli/test_cursor.py | 8 +- tests/cli/test_run.py | 22 +- 10 files changed, 257 insertions(+), 151 deletions(-) diff --git a/docs/patterns/cli.mdx b/docs/patterns/cli.mdx index b39aec2bb..12f6cbb11 100644 --- a/docs/patterns/cli.mdx +++ b/docs/patterns/cli.mdx @@ -18,15 +18,13 @@ fastmcp --help | Command | Purpose | Dependency Management | | ------- | ------- | --------------------- | -| `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 | +| `run` | Run a FastMCP server directly | **Supports:** Local files, factory functions, URLs, MCP configs. **Deps:** 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 | **Supports:** Local files only. **Deps:** 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 | **Supports:** Local files only. **Deps:** Creates an isolated environment; dependencies must be explicitly specified with `--with` and/or `--with-editable` | +| `inspect` | Generate a JSON report about a FastMCP server | **Supports:** Local files only. **Deps:** Uses your current environment; you are responsible for ensuring all dependencies are available | | `version` | Display version information | N/A | -## Command Details - -### `run` +## `fastmcp run` Run a FastMCP server directly or proxy a remote server. @@ -38,7 +36,7 @@ fastmcp run server.py 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 +### Options | Option | Flag | Description | | ------ | ---- | ----------- | @@ -54,106 +52,125 @@ By default, this command runs the server directly in your current Python environ | Requirements File | `--with-requirements` | Requirements file to install dependencies from | -#### Server Specification +### Entrypoints -The server can be specified in four ways: -1. `server.py` - imports the module and looks for a FastMCP object named `mcp`, `server`, or `app`. Errors if no such object is found. -2. `server.py:custom_name` - imports and uses the specified server object -3. `http://server-url/path` or `https://server-url/path` - connects to a remote server and creates a proxy -4. `mcp.json` - runs servers defined in a standard MCP configuration file +The `fastmcp run` command supports the following entrypoints: - -When using `fastmcp run` with a local file, it **ignores** the `if __name__ == "__main__"` block entirely. Instead, it finds your server object and calls its `run()` method directly with the transport options you specify. This means you can use `fastmcp run` to override the transport specified in your code. - +1. **[Inferred server instance](#inferred-server-instance)**: `server.py` - imports the module and looks for a FastMCP object named `mcp`, `server`, or `app`. Errors if no such object is found. +2. **[Explicit server object](#explicit-server-object)**: `server.py:custom_name` - imports and uses the specified server object +3. **[Factory function](#factory-function)**: `server.py:create_server` - calls the specified function (sync or async) to create a server instance +4. **[Remote server proxy](#remote-server-proxy)**: `https://example.com/mcp-server` - connects to a remote server and creates a **local proxy server** +5. **MCP configuration file**: `mcp.json` - runs servers defined in a standard MCP configuration file -For example, if your code contains: + +Note: When using `fastmcp run` with a local file, it **completely ignores** the `if __name__ == "__main__"` block. This means: +- Any setup code in `__main__` will NOT run +- Server configuration in `__main__` is bypassed +- `fastmcp run` finds your server object/factory and runs it with its own transport settings -```python -# server.py +If you need setup code to run, use the **factory pattern** instead. + + +#### Inferred Server Instance + +If you provide a path to a file, `fastmcp run` will load the file and look for a FastMCP server instance stored as a variable named `mcp`, `server`, or `app`. If no such object is found, it will raise an error. + +For example, if you have a file called `server.py` with the following content: + +```python server.py from fastmcp import FastMCP mcp = FastMCP("MyServer") - -@mcp.tool -def hello(name: str) -> str: - return f"Hello, {name}!" - -if __name__ == "__main__": - # This is ignored when using `fastmcp run`! - mcp.run(transport="stdio") ``` -You can run it with Streamable HTTP transport regardless of what's in the `__main__` block: +You can run it with: ```bash -fastmcp run server.py --transport http --port 8000 +fastmcp run server.py ``` -**Examples** +#### Explicit Server Object + +If your server is stored as a variable with a custom name, or you want to be explicit about which server to run, you can use the following syntax to load a specific server object: ```bash -# Run a local server with Streamable HTTP transport on a custom port -fastmcp run server.py --transport http --port 8000 - -# Connect to a remote server and proxy as a stdio server -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 +fastmcp run server.py:custom_name ``` -#### Running MCP Configuration Files +For example, if you have a file called `server.py` with the following content: -FastMCP can run servers defined in standard MCP configuration files (typically named `mcp.json`). When you run an mcp.json file, FastMCP creates a proxy server that runs all the servers referenced in the configuration. +```python +from fastmcp import FastMCP -**Example mcp.json:** -```json -{ - "mcpServers": { - "fetch": { - "command": "uvx", - "args": [ - "mcp-server-fetch" - ] - }, - "filesystem": { - "command": "npx", - "args": [ - "-y", - "@modelcontextprotocol/server-filesystem", - "/Users/username/Documents" - ] - } - } -} +my_server = FastMCP("CustomServer") + +@my_server.tool +def hello() -> str: + return "Hello from custom server!" ``` -**Run the configuration:** +You can run it with: + +```bash +fastmcp run server.py:custom_name +``` + +#### Factory Function + + +Since `fastmcp run` ignores the `if __name__ == "__main__"` block, you can use a factory function to run setup code before your server starts. Factory functions are called without any arguments and must return a FastMCP server instance. Both sync and async factory functions are supported. + +The syntax for using a factory function is the same as for an explicit server object: `fastmcp run server.py:factory_fn`. FastMCP will automatically detect that you have identified a function rather than a server Instance + +For example, if you have a file called `server.py` with the following content: + +```python +from fastmcp import FastMCP + +async def create_server() -> FastMCP: + mcp = FastMCP("MyServer") + + @mcp.tool + def add(x: int, y: int) -> int: + return x + y + + # Setup that runs with fastmcp run + tool = await mcp.get_tool("add") + tool.disable() + + return mcp +``` + +You can run it with: + +```bash +fastmcp run server.py:create_server +``` + +#### Remote Server Proxy + +FastMCP run can also start a local proxy server that connects to a remote server. This is useful when you want to run a remote server locally for testing or development purposes, or to use with a client that doesn't support direct connections to remote servers. + +To start a local proxy, you can use the following syntax: + +```bash +fastmcp run https://example.com/mcp +``` + +#### MCP Configuration + +FastMCP can also run servers defined in a standard MCP configuration file. This is useful when you want to run multiple servers from a single file, or when you want to use a client that doesn't support direct connections to remote servers. + +To run a MCP configuration file, you can use the following syntax: + ```bash -# Run with default stdio transport fastmcp run mcp.json - -# Run with HTTP transport on custom port -fastmcp run mcp.json --transport http --port 8080 - -# Run with SSE transport -fastmcp run mcp.json --transport sse ``` -### `dev` +This will run all the servers defined in the file. + +## `fastmcp dev` Run a MCP server with the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) for testing. @@ -182,7 +199,7 @@ This command does not support HTTP testing. To test a server over Streamable HTT 2. Open the MCP Inspector separately and connect to your running server -#### Options +### Options | Option | Flag | Description | | ------ | ---- | ----------- | @@ -195,6 +212,18 @@ This command does not support HTTP testing. To test a server over Streamable HTT | Project Directory | `--project` | Run the command within the given project directory | | Requirements File | `--with-requirements` | Requirements file to install dependencies from | +### Entrypoints + +The `dev` command supports local FastMCP server files only: + +1. **Inferred server instance**: `server.py` - imports the module and looks for a FastMCP object named `mcp`, `server`, or `app`. Errors if no such object is found. +2. **Explicit server object**: `server.py:custom_name` - imports and uses the specified server object +3. **Factory function**: `server.py:create_server` - calls the specified function (sync or async) to create a server instance + + +The `dev` command **only supports local files** - no URLs, remote servers, or MCP configuration files. + + **Examples** ```bash @@ -211,7 +240,7 @@ fastmcp dev server.py --with-requirements requirements.txt fastmcp dev server.py --project /path/to/project ``` -### `install` +## `fastmcp install` Install a MCP server in MCP client applications. FastMCP currently supports the following clients: @@ -242,14 +271,7 @@ Note that for security reasons, MCP clients usually run every server in a comple **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. -#### Server Specification - -The `install` command supports the same `file.py:object` notation as the `run` command: - -1. `server.py` - imports the module and looks for a FastMCP object named `mcp`, `server`, or `app`. Errors if no such object is found. -2. `server.py:custom_name` - imports and uses the specified server object - -#### Options +### Options | Option | Flag | Description | | ------ | ---- | ----------- | @@ -262,6 +284,22 @@ The `install` command supports the same `file.py:object` notation as the `run` c | Project Directory | `--project` | Run the command within the given project directory | | Requirements File | `--with-requirements` | Requirements file to install dependencies from | +### Entrypoints + +The `install` command supports local FastMCP server files only: + +1. **Inferred server instance**: `server.py` - imports the module and looks for a FastMCP object named `mcp`, `server`, or `app`. Errors if no such object is found. +2. **Explicit server object**: `server.py:custom_name` - imports and uses the specified server object +3. **Factory function**: `server.py:create_server` - calls the specified function (sync or async) to create a server instance + + +Factory functions are particularly useful for install commands since they allow setup code to run that would otherwise be ignored when the MCP client runs your server. + + + +The `install` command **only supports local files** - no URLs, remote servers, or MCP configuration files. For remote servers, use your MCP client's native configuration. + + **Examples** ```bash @@ -299,7 +337,7 @@ fastmcp install mcp-json server.py --name "My Server" --with pandas fastmcp install mcp-json server.py --copy ``` -#### MCP JSON Generation +### MCP JSON Generation The `mcp-json` subcommand generates standard MCP JSON configuration that can be used with any MCP-compatible client. This is useful when: @@ -339,7 +377,7 @@ To use this configuration with your MCP client, you'll typically need to add it | ------ | ---- | ----------- | | Copy to Clipboard | `--copy` | Copy configuration to clipboard instead of printing to stdout | -### `inspect` +## `fastmcp inspect` @@ -349,7 +387,25 @@ Generate a detailed JSON report about a FastMCP server, including information ab fastmcp inspect server.py ``` -The command supports the same server specification format as `run` and `install`: +### Options + +| Option | Flag | Description | +| ------ | ---- | ----------- | +| Output File | `--output`, `-o` | Output file path for the JSON report (default: server-info.json) | + +### Entrypoints + +The `inspect` command supports local FastMCP server files only: + +1. **Inferred server instance**: `server.py` - imports the module and looks for a FastMCP object named `mcp`, `server`, or `app`. Errors if no such object is found. +2. **Explicit server object**: `server.py:custom_name` - imports and uses the specified server object +3. **Factory function**: `server.py:create_server` - calls the specified function (sync or async) to create a server instance + + +The `inspect` command **only supports local files** - no URLs, remote servers, or MCP configuration files. + + +**Examples** ```bash # Auto-detect server object @@ -362,7 +418,7 @@ fastmcp inspect server.py:my_server fastmcp inspect server.py --output analysis.json ``` -### `version` +## `fastmcp version` Display version information about FastMCP and related components. @@ -370,7 +426,7 @@ Display version information about FastMCP and related components. fastmcp version ``` -#### Options +### Options | Option | Flag | Description | | ------ | ---- | ----------- | diff --git a/src/fastmcp/cli/cli.py b/src/fastmcp/cli/cli.py index 99e37107a..a5390d2b0 100644 --- a/src/fastmcp/cli/cli.py +++ b/src/fastmcp/cli/cli.py @@ -138,7 +138,7 @@ def version( @app.command -def dev( +async def dev( server_spec: str, *, with_editable: Annotated[ @@ -220,7 +220,7 @@ def dev( try: # Import server to get dependencies - server: FastMCP = run_module.import_server(file, server_object) + server: FastMCP = await run_module.import_server(file, server_object) if server.dependencies is not None: with_packages = list(set(with_packages + server.dependencies)) @@ -283,7 +283,7 @@ def dev( @app.command -def run( +async def run( server_spec: str, *server_args: str, transport: Annotated[ @@ -414,7 +414,7 @@ def run( else: # Use direct import for backwards compatibility try: - run_module.run_command( + await run_module.run_command( server_spec=server_spec, transport=transport, host=host, @@ -476,7 +476,7 @@ async def inspect( try: # Import the server - server = run_module.import_server(file, server_object) + server = await run_module.import_server(file, server_object) # Get server information - using native async support info = await inspect_fastmcp(server) diff --git a/src/fastmcp/cli/install/claude_code.py b/src/fastmcp/cli/install/claude_code.py index 3b728af26..490de9545 100644 --- a/src/fastmcp/cli/install/claude_code.py +++ b/src/fastmcp/cli/install/claude_code.py @@ -167,7 +167,7 @@ def install_claude_code( return False -def claude_code_command( +async def claude_code_command( server_spec: str, *, server_name: Annotated[ @@ -234,7 +234,7 @@ def claude_code_command( Args: server_spec: Python file to install, optionally with :object suffix """ - file, server_object, name, packages, env_dict = process_common_args( + 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 134560a32..56f5d2bc8 100644 --- a/src/fastmcp/cli/install/claude_desktop.py +++ b/src/fastmcp/cli/install/claude_desktop.py @@ -140,7 +140,7 @@ def install_claude_desktop( return False -def claude_desktop_command( +async def claude_desktop_command( server_spec: str, *, server_name: Annotated[ @@ -207,7 +207,7 @@ def claude_desktop_command( Args: server_spec: Python file to install, optionally with :object suffix """ - file, server_object, name, with_packages, env_dict = process_common_args( + 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 654e5e0e8..87527ce73 100644 --- a/src/fastmcp/cli/install/cursor.py +++ b/src/fastmcp/cli/install/cursor.py @@ -150,7 +150,7 @@ def install_cursor( return False -def cursor_command( +async def cursor_command( server_spec: str, *, server_name: Annotated[ @@ -217,7 +217,7 @@ def cursor_command( Args: server_spec: Python file to install, optionally with :object suffix """ - file, server_object, name, with_packages, env_dict = process_common_args( + 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 1afaa7a3b..b5145c18d 100644 --- a/src/fastmcp/cli/install/mcp_json.py +++ b/src/fastmcp/cli/install/mcp_json.py @@ -113,7 +113,7 @@ def install_mcp_json( return False -def mcp_json_command( +async def mcp_json_command( server_spec: str, *, server_name: Annotated[ @@ -188,7 +188,7 @@ def mcp_json_command( Args: server_spec: Python file to install, optionally with :object suffix """ - file, server_object, name, packages, env_dict = process_common_args( + 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 a56bc595b..e611e3d7e 100644 --- a/src/fastmcp/cli/install/shared.py +++ b/src/fastmcp/cli/install/shared.py @@ -23,7 +23,7 @@ def parse_env_var(env_var: str) -> tuple[str, str]: return key.strip(), value.strip() -def process_common_args( +async def process_common_args( server_spec: str, server_name: str | None, with_packages: list[str], @@ -49,7 +49,7 @@ def process_common_args( server = None if not name: try: - server = import_server(file, server_object) + server = await import_server(file, server_object) name = server.name except (ImportError, ModuleNotFoundError) as e: logger.debug( diff --git a/src/fastmcp/cli/run.py b/src/fastmcp/cli/run.py index 5df022529..9a0a47971 100644 --- a/src/fastmcp/cli/run.py +++ b/src/fastmcp/cli/run.py @@ -1,6 +1,7 @@ """FastMCP run command implementation with enhanced type hints.""" import importlib.util +import inspect import json import re import subprocess @@ -58,15 +59,15 @@ def parse_file_path(server_spec: str) -> tuple[Path, str | None]: return file_path, server_object -def import_server(file: Path, server_object: str | None = None) -> Any: +async def import_server(file: Path, server_or_factory: str | None = None) -> Any: """Import a MCP server from a file. Args: file: Path to the file - server_object: Optional object name in format "module:object" or just "object" + server_or_factory: Optional object name in format "module:object" or just "object" Returns: - The server object + The server object (or result of calling a factory function) """ # Add parent directory to Python path so imports can be resolved file_dir = str(file.parent) @@ -86,11 +87,12 @@ def import_server(file: Path, server_object: str | None = None) -> Any: spec.loader.exec_module(module) # If no object specified, try common server names - if not server_object: - # Look for the most common server object names + if not server_or_factory: + # Look for common server instance names for name in ["mcp", "server", "app"]: if hasattr(module, name): - return getattr(module, name) + obj = getattr(module, name) + return await _resolve_server_or_factory(obj, file, name) logger.error( f"No server object found in {file}. Please either:\n" @@ -100,14 +102,14 @@ def import_server(file: Path, server_object: str | None = None) -> Any: ) sys.exit(1) - assert server_object is not None + assert server_or_factory is not None # Handle module:object syntax - if ":" in server_object: - module_name, object_name = server_object.split(":", 1) + if ":" in server_or_factory: + module_name, object_name = server_or_factory.split(":", 1) try: server_module = importlib.import_module(module_name) - server = getattr(server_module, object_name, None) + obj = getattr(server_module, object_name, None) except ImportError: logger.error( f"Could not import module '{module_name}'", @@ -116,16 +118,62 @@ def import_server(file: Path, server_object: str | None = None) -> Any: sys.exit(1) else: # Just object name - server = getattr(module, server_object, None) + obj = getattr(module, server_or_factory, None) - if server is None: + if obj is None: logger.error( - f"Server object '{server_object}' not found", + f"Server object '{server_or_factory}' not found", extra={"file": str(file)}, ) sys.exit(1) - return server + return await _resolve_server_or_factory(obj, file, server_or_factory) + + +async def _resolve_server_or_factory(obj: Any, file: Path, name: str) -> Any: + """Resolve a server object or factory function to a server instance. + + Args: + obj: The object that might be a server or factory function + file: Path to the file for error messages + name: Name of the object for error messages + + Returns: + A server instance + """ + # Check if it's a function or coroutine function + if inspect.isfunction(obj) or inspect.iscoroutinefunction(obj): + logger.debug(f"Found factory function '{name}' in {file}") + + try: + if inspect.iscoroutinefunction(obj): + # Async factory function + server = await obj() + else: + # Sync factory function + server = obj() + + # Validate the result is a FastMCP server + if not isinstance(server, FastMCP | FastMCP1x): + logger.error( + f"Factory function '{name}' must return a FastMCP server instance, " + f"got {type(server).__name__}", + extra={"file": str(file)}, + ) + sys.exit(1) + + logger.debug(f"Factory function '{name}' created server: {server.name}") + return server + + except Exception as e: + logger.error( + f"Failed to call factory function '{name}': {e}", + extra={"file": str(file)}, + ) + sys.exit(1) + + # Not a function, return as-is (should be a server instance) + return obj def run_with_uv( @@ -219,7 +267,7 @@ def create_client_server(url: str) -> Any: import fastmcp client = fastmcp.Client(url) - server = fastmcp.FastMCP.from_client(client) + server = fastmcp.FastMCP.as_proxy(client) return server except Exception as e: logger.error(f"Failed to create client for URL {url}: {e}") @@ -237,14 +285,16 @@ def create_mcp_config_server(mcp_config_path: Path) -> FastMCP[None]: return server -def import_server_with_args( - file: Path, server_object: str | None = None, server_args: list[str] | None = None +async def import_server_with_args( + file: Path, + server_or_factory: str | None = None, + server_args: list[str] | None = None, ) -> Any: """Import a server with optional command line arguments. Args: file: Path to the server file - server_object: Optional server object name + server_or_factory: Optional server object or factory function name server_args: Optional command line arguments to inject Returns: @@ -254,14 +304,14 @@ def import_server_with_args( original_argv = sys.argv[:] try: sys.argv = [str(file)] + server_args - return import_server(file, server_object) + return await import_server(file, server_or_factory) finally: sys.argv = original_argv else: - return import_server(file, server_object) + return await import_server(file, server_or_factory) -def run_command( +async def run_command( server_spec: str, transport: TransportType | None = None, host: str | None = None, @@ -293,8 +343,8 @@ def run_command( server = create_mcp_config_server(Path(server_spec)) else: # Handle file case - file, server_object = parse_file_path(server_spec) - server = import_server_with_args(file, server_object, server_args) + file, server_or_factory = parse_file_path(server_spec) + server = await import_server_with_args(file, server_or_factory, server_args) logger.debug(f'Found server "{server.name}" in {file}') # Run the server @@ -320,7 +370,7 @@ def run_command( kwargs["show_banner"] = False try: - server.run(**kwargs) + await server.run_async(**kwargs) except Exception as e: logger.error(f"Failed to run server: {e}") sys.exit(1) diff --git a/tests/cli/test_cursor.py b/tests/cli/test_cursor.py index 2383f211a..de52f17e1 100644 --- a/tests/cli/test_cursor.py +++ b/tests/cli/test_cursor.py @@ -306,7 +306,7 @@ class TestCursorCommand: @patch("fastmcp.cli.install.cursor.install_cursor") @patch("fastmcp.cli.install.cursor.process_common_args") - def test_cursor_command_basic(self, mock_process_args, mock_install): + async def test_cursor_command_basic(self, mock_process_args, mock_install): """Test basic cursor command execution.""" mock_process_args.return_value = ( Path("server.py"), @@ -318,7 +318,7 @@ class TestCursorCommand: mock_install.return_value = True with patch("sys.exit") as mock_exit: - cursor_command("server.py") + await cursor_command("server.py") mock_install.assert_called_once_with( file=Path("server.py"), @@ -335,7 +335,7 @@ class TestCursorCommand: @patch("fastmcp.cli.install.cursor.install_cursor") @patch("fastmcp.cli.install.cursor.process_common_args") - def test_cursor_command_failure(self, mock_process_args, mock_install): + async def test_cursor_command_failure(self, mock_process_args, mock_install): """Test cursor command when installation fails.""" mock_process_args.return_value = ( Path("server.py"), @@ -347,6 +347,6 @@ class TestCursorCommand: mock_install.return_value = False with pytest.raises(SystemExit) as exc_info: - cursor_command("server.py") + await cursor_command("server.py") assert exc_info.value.code == 1 diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py index 6ae06f1eb..d6d2578ce 100644 --- a/tests/cli/test_run.py +++ b/tests/cli/test_run.py @@ -157,7 +157,7 @@ def greet(name: str) -> str: return f"Hello, {name}!" """) - server = import_server(test_file) + server = await import_server(test_file) assert server.name == "TestServer" tools = await server.get_tools() assert "greet" in tools @@ -178,12 +178,12 @@ if __name__ == "__main__": app.run() """) - server = import_server(test_file) + server = await import_server(test_file) assert server.name == "MainServer" tools = await server.get_tools() assert "calculate" in tools - def test_import_server_standard_names(self, tmp_path): + async def test_import_server_standard_names(self, tmp_path): """Test automatic detection of standard names (mcp, server, app).""" # Test with 'mcp' name mcp_file = tmp_path / "mcp_server.py" @@ -192,7 +192,7 @@ import fastmcp mcp = fastmcp.FastMCP("MCPServer") """) - server = import_server(mcp_file) + server = await import_server(mcp_file) assert server.name == "MCPServer" # Test with 'server' name @@ -202,7 +202,7 @@ import fastmcp server = fastmcp.FastMCP("ServerServer") """) - server = import_server(server_file) + server = await import_server(server_file) assert server.name == "ServerServer" # Test with 'app' name @@ -212,7 +212,7 @@ import fastmcp app = fastmcp.FastMCP("AppServer") """) - server = import_server(app_file) + server = await import_server(app_file) assert server.name == "AppServer" async def test_import_server_nonstandard_name(self, tmp_path): @@ -228,12 +228,12 @@ def custom_tool() -> str: return "custom" """) - server = import_server(test_file, "my_custom_server") + server = await import_server(test_file, "my_custom_server") assert server.name == "CustomServer" tools = await server.get_tools() assert "custom_tool" in tools - def test_import_server_no_standard_names_fails(self, tmp_path): + async def test_import_server_no_standard_names_fails(self, tmp_path): """Test importing server when no standard names exist fails.""" test_file = tmp_path / "server.py" test_file.write_text(""" @@ -243,10 +243,10 @@ other_name = fastmcp.FastMCP("OtherServer") """) with pytest.raises(SystemExit) as exc_info: - import_server(test_file) + await import_server(test_file) assert exc_info.value.code == 1 - def test_import_server_nonexistent_object_fails(self, tmp_path): + async def test_import_server_nonexistent_object_fails(self, tmp_path): """Test importing nonexistent server object fails.""" test_file = tmp_path / "server.py" test_file.write_text(""" @@ -256,5 +256,5 @@ mcp = fastmcp.FastMCP("TestServer") """) with pytest.raises(SystemExit) as exc_info: - import_server(test_file, "nonexistent") + await import_server(test_file, "nonexistent") assert exc_info.value.code == 1