mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-20 04:24:17 +02:00
Support factory functions in fastmcp run (#1384)
This commit is contained in:
parent
78e9bab244
commit
d52d6d8fff
10 changed files with 249 additions and 143 deletions
|
|
@ -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.
|
||||
</Tip>
|
||||
|
||||
#### 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
|
||||
<VersionBadge version="2.3.5" />
|
||||
|
||||
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:
|
||||
|
||||
<Tip>
|
||||
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.
|
||||
</Tip>
|
||||
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:
|
||||
<Warning>
|
||||
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.
|
||||
</Warning>
|
||||
|
||||
#### 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
|
||||
<VersionBadge version="2.11.2" />
|
||||
|
||||
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
|
||||
</Warning>
|
||||
|
||||
#### 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
|
||||
|
||||
<Warning>
|
||||
The `dev` command **only supports local files** - no URLs, remote servers, or MCP configuration files.
|
||||
</Warning>
|
||||
|
||||
**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`
|
||||
<VersionBadge version="2.10.3" />
|
||||
|
||||
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.
|
||||
</Tip>
|
||||
|
||||
#### 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
|
||||
|
||||
<Note>
|
||||
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.
|
||||
</Note>
|
||||
|
||||
<Warning>
|
||||
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.
|
||||
</Warning>
|
||||
|
||||
**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`
|
||||
|
||||
<VersionBadge version="2.9.0" />
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
<Warning>
|
||||
The `inspect` command **only supports local files** - no URLs, remote servers, or MCP configuration files.
|
||||
</Warning>
|
||||
|
||||
**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 |
|
||||
| ------ | ---- | ----------- |
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue