diff --git a/docs/docs.json b/docs/docs.json
index 1054a0289..9687185dc 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -376,10 +376,31 @@
"group": "fastmcp.utilities",
"pages": [
"python-sdk/fastmcp-utilities-__init__",
+ "python-sdk/fastmcp-utilities-auth",
"python-sdk/fastmcp-utilities-cli",
"python-sdk/fastmcp-utilities-components",
"python-sdk/fastmcp-utilities-exceptions",
- "python-sdk/fastmcp-utilities-fastmcp_config",
+ {
+ "group": "fastmcp_config",
+ "pages": [
+ "python-sdk/fastmcp-utilities-fastmcp_config-__init__",
+ {
+ "group": "v1",
+ "pages": [
+ "python-sdk/fastmcp-utilities-fastmcp_config-v1-__init__",
+ "python-sdk/fastmcp-utilities-fastmcp_config-v1-fastmcp_config",
+ {
+ "group": "sources",
+ "pages": [
+ "python-sdk/fastmcp-utilities-fastmcp_config-v1-sources-__init__",
+ "python-sdk/fastmcp-utilities-fastmcp_config-v1-sources-base",
+ "python-sdk/fastmcp-utilities-fastmcp_config-v1-sources-filesystem"
+ ]
+ }
+ ]
+ }
+ ]
+ },
"python-sdk/fastmcp-utilities-http",
"python-sdk/fastmcp-utilities-inspect",
"python-sdk/fastmcp-utilities-json_schema",
diff --git a/docs/integrations/azure.mdx b/docs/integrations/azure.mdx
index ad8fc31a9..890e648e3 100644
--- a/docs/integrations/azure.mdx
+++ b/docs/integrations/azure.mdx
@@ -212,7 +212,7 @@ Redirect path configured in your Azure App registration
-Comma-separated list of required Microsoft Graph scopes
+Comma-, space-, or JSON-separated list of required Microsoft Graph scopes
diff --git a/docs/integrations/github.mdx b/docs/integrations/github.mdx
index b849e1bd4..0d05126f7 100644
--- a/docs/integrations/github.mdx
+++ b/docs/integrations/github.mdx
@@ -169,7 +169,7 @@ Redirect path configured in your GitHub OAuth App
-Comma-separated list of required GitHub scopes (e.g., `user,repo`)
+Comma-, space-, or JSON-separated list of required GitHub scopes (e.g., `user repo` or `["user","repo"]`)
diff --git a/docs/integrations/google.mdx b/docs/integrations/google.mdx
index 14a79a80c..c3ff3cc7b 100644
--- a/docs/integrations/google.mdx
+++ b/docs/integrations/google.mdx
@@ -179,7 +179,7 @@ Redirect path configured in your Google OAuth Client
-Comma-separated list of required Google scopes (e.g., `openid`)
+Comma-, space-, or JSON-separated list of required Google scopes (e.g., `openid,profile` or `["openid","profile"]`)
diff --git a/docs/integrations/workos-oauth.mdx b/docs/integrations/workos-oauth.mdx
index 34e9198f4..fee2b1c46 100644
--- a/docs/integrations/workos-oauth.mdx
+++ b/docs/integrations/workos-oauth.mdx
@@ -166,7 +166,7 @@ Redirect path configured in your WorkOS OAuth App
-List of required OAuth scopes (e.g., `["openid", "profile", "email"]`)
+Comma-, space-, or JSON-separated list of required OAuth scopes (e.g., `openid profile email` or `["openid","profile","email"]`)
diff --git a/docs/patterns/cli.mdx b/docs/patterns/cli.mdx
index 70443e616..66aeac978 100644
--- a/docs/patterns/cli.mdx
+++ b/docs/patterns/cli.mdx
@@ -436,17 +436,45 @@ To use this configuration with your MCP client, you'll typically need to add it
-Generate a detailed JSON report about a FastMCP server, including information about its tools, prompts, resources, and capabilities.
+Inspect a FastMCP server to view summary information or generate a detailed JSON report.
```bash
+# Show text summary
fastmcp inspect server.py
+
+# Output FastMCP JSON to stdout
+fastmcp inspect server.py --format fastmcp
+
+# Save MCP JSON to file (format required with -o)
+fastmcp inspect server.py --format mcp -o manifest.json
```
### Options
| Option | Flag | Description |
| ------ | ---- | ----------- |
-| Output File | `--output`, `-o` | Output file path for the JSON report (default: server-info.json) |
+| Format | `--format`, `-f` | Output format: `fastmcp` (FastMCP-specific) or `mcp` (MCP protocol). Required when using `-o` |
+| Output File | `--output`, `-o` | Save JSON report to file instead of stdout. Requires `--format` |
+
+### Output Formats
+
+#### FastMCP Format (`--format fastmcp`)
+The default and most comprehensive format, includes all FastMCP-specific metadata:
+- Server name, instructions, and version
+- FastMCP version and MCP version
+- Tool tags and enabled status
+- Output schemas for tools
+- Annotations and custom metadata
+- Uses snake_case field names
+- **Use this for**: Complete server introspection and debugging FastMCP servers
+
+#### MCP Protocol Format (`--format mcp`)
+Shows exactly what MCP clients will see via the protocol:
+- Only includes standard MCP protocol fields
+- Matches output from `client.list_tools()`, `client.list_prompts()`, etc.
+- Uses camelCase field names (e.g., `inputSchema`)
+- Excludes FastMCP-specific fields like tags and enabled status
+- **Use this for**: Debugging client visibility and ensuring MCP compatibility
### Entrypoints
@@ -461,17 +489,46 @@ The `inspect` command supports local FastMCP server files and configuration:
The `inspect` command **only supports local files and fastmcp.json** - no URLs, remote servers, or standard MCP configuration files.
-**Examples**
+### Examples
```bash
-# Auto-detect server entrypoint
+# Show text summary (no JSON output)
fastmcp inspect server.py
+# Output:
+# Server: MyServer
+# Instructions: A helpful MCP server
+# Version: 1.0.0
+#
+# Components:
+# Tools: 5
+# Prompts: 2
+# Resources: 3
+# Templates: 1
+#
+# Environment:
+# FastMCP: 2.0.0
+# MCP: 1.0.0
+#
+# Use --format [fastmcp|mcp] for complete JSON output
+
+# Output FastMCP format to stdout
+fastmcp inspect server.py --format fastmcp
# Specify server entrypoint
fastmcp inspect server.py:my_server
-# Custom output location
-fastmcp inspect server.py --output analysis.json
+# Output MCP protocol format to stdout
+fastmcp inspect server.py --format mcp
+
+# Save to file (format required)
+fastmcp inspect server.py --format fastmcp -o server-manifest.json
+
+# Save MCP format with custom server object
+fastmcp inspect server.py:my_server --format mcp -o mcp-manifest.json
+
+# Error: format required with output file
+fastmcp inspect server.py -o output.json
+# Error: --format is required when using -o/--output
```
## `fastmcp project prepare`
diff --git a/docs/public/schemas/fastmcp.json/latest.json b/docs/public/schemas/fastmcp.json/latest.json
index 81d0ad754..c28a50452 100644
--- a/docs/public/schemas/fastmcp.json/latest.json
+++ b/docs/public/schemas/fastmcp.json/latest.json
@@ -243,17 +243,25 @@
"editable": {
"anyOf": [
{
- "type": "string"
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
},
{
"type": "null"
}
],
"default": null,
- "description": "Directory to install in editable mode",
+ "description": "Directories to install in editable mode",
"examples": [
- ".",
- "../my-package"
+ [
+ ".",
+ "../my-package"
+ ],
+ [
+ "/path/to/package"
+ ]
],
"title": "Editable"
}
diff --git a/docs/public/schemas/fastmcp.json/v1.json b/docs/public/schemas/fastmcp.json/v1.json
index 81d0ad754..c28a50452 100644
--- a/docs/public/schemas/fastmcp.json/v1.json
+++ b/docs/public/schemas/fastmcp.json/v1.json
@@ -243,17 +243,25 @@
"editable": {
"anyOf": [
{
- "type": "string"
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
},
{
"type": "null"
}
],
"default": null,
- "description": "Directory to install in editable mode",
+ "description": "Directories to install in editable mode",
"examples": [
- ".",
- "../my-package"
+ [
+ ".",
+ "../my-package"
+ ],
+ [
+ "/path/to/package"
+ ]
],
"title": "Editable"
}
diff --git a/docs/python-sdk/fastmcp-cli-claude.mdx b/docs/python-sdk/fastmcp-cli-claude.mdx
index 6841a69dc..82eb6e881 100644
--- a/docs/python-sdk/fastmcp-cli-claude.mdx
+++ b/docs/python-sdk/fastmcp-cli-claude.mdx
@@ -32,7 +32,7 @@ Add or update a FastMCP server in Claude's configuration.
**Args:**
- `file_spec`: Path to the server file, optionally with \:object suffix
- `server_name`: Name for the server in Claude's config
-- `with_editable`: Optional directory to install in editable mode
+- `with_editable`: Optional list of directories to install in editable mode
- `with_packages`: Optional list of additional packages to install
- `env_vars`: Optional dictionary of environment variables. These are merged with
any existing variables, with new values taking precedence.
diff --git a/docs/python-sdk/fastmcp-cli-cli.mdx b/docs/python-sdk/fastmcp-cli-cli.mdx
index f9d027f40..60838c769 100644
--- a/docs/python-sdk/fastmcp-cli-cli.mdx
+++ b/docs/python-sdk/fastmcp-cli-cli.mdx
@@ -10,7 +10,24 @@ FastMCP CLI tools using Cyclopts.
## Functions
-### `version`
+### `with_argv`
+
+```python
+with_argv(args: list[str] | None)
+```
+
+
+Temporarily replace sys.argv if args provided.
+
+This context manager is used at the CLI boundary to inject
+server arguments when needed, without mutating sys.argv deep
+in the source loading logic.
+
+Args are provided without the script name, so we preserve sys.argv[0]
+and replace the rest.
+
+
+### `version`
```python
version()
@@ -20,7 +37,7 @@ version()
Display version information and platform details.
-### `dev`
+### `dev`
```python
dev(server_spec: str | None = None) -> None
@@ -33,7 +50,7 @@ Run an MCP server with the MCP Inspector for development.
- `server_spec`: Python file to run, optionally with \:object suffix, or None to auto-detect fastmcp.json
-### `run`
+### `run`
```python
run(server_spec: str | None = None, *server_args: str) -> None
@@ -57,28 +74,56 @@ fastmcp run server.py -- --config config.json --debug
- `server_spec`: Python file, object specification (file\:obj), config file, URL, or None to auto-detect
-### `inspect`
+### `inspect`
```python
inspect(server_spec: str | None = None) -> None
```
-Inspect an MCP server and generate a JSON report.
+Inspect an MCP server and display information or generate a JSON report.
-This command analyzes an MCP server and generates a comprehensive JSON report
-containing information about the server's name, instructions, version, tools,
-prompts, resources, templates, and capabilities.
+This command analyzes an MCP server. Without flags, it displays a text summary.
+Use --format to output complete JSON data.
**Examples:**
+# Show text summary
fastmcp inspect server.py
-fastmcp inspect server.py -o report.json
-fastmcp inspect server.py:mcp -o analysis.json
-fastmcp inspect path/to/server.py:app -o /tmp/server-info.json
+
+# Output FastMCP format JSON to stdout
+fastmcp inspect server.py --format fastmcp
+
+# Save MCP protocol format to file (format required with -o)
+fastmcp inspect server.py --format mcp -o manifest.json
+
+# Inspect from fastmcp.json configuration
fastmcp inspect fastmcp.json
fastmcp inspect # auto-detect fastmcp.json
**Args:**
- `server_spec`: Python file to inspect, optionally with \:object suffix, or fastmcp.json
+
+### `prepare`
+
+```python
+prepare(config_path: Annotated[str | None, cyclopts.Parameter(help='Path to fastmcp.json configuration file')] = None, output_dir: Annotated[str | None, cyclopts.Parameter(help='Directory to create the persistent environment in')] = None, skip_source: Annotated[bool, cyclopts.Parameter(help='Skip source preparation (e.g., git clone)')] = False) -> None
+```
+
+
+Prepare a FastMCP project by creating a persistent uv environment.
+
+This command creates a persistent uv project with all dependencies installed:
+- Creates a pyproject.toml with dependencies from the config
+- Installs all Python packages into a .venv
+- Prepares the source (git clone, download, etc.) unless --skip-source
+
+After running this command, you can use:
+fastmcp run <config> --project <output-dir>
+
+This is useful for:
+- CI/CD pipelines with separate build and run stages
+- Docker images where you prepare during build
+- Production deployments where you want fast startup times
+
diff --git a/docs/python-sdk/fastmcp-cli-install-claude_code.mdx b/docs/python-sdk/fastmcp-cli-install-claude_code.mdx
index d85264f05..fd1d888ff 100644
--- a/docs/python-sdk/fastmcp-cli-install-claude_code.mdx
+++ b/docs/python-sdk/fastmcp-cli-install-claude_code.mdx
@@ -46,7 +46,7 @@ Install FastMCP server in Claude Code.
- `file`: Path to the server file
- `server_object`: Optional server object name (for \:object suffix)
- `name`: Name for the server in Claude Code
-- `with_editable`: Optional directory to install in editable mode
+- `with_editable`: Optional list of directories to install in editable mode
- `with_packages`: Optional list of additional packages to install
- `env_vars`: Optional dictionary of environment variables
- `python_version`: Optional Python version to use
diff --git a/docs/python-sdk/fastmcp-cli-install-claude_desktop.mdx b/docs/python-sdk/fastmcp-cli-install-claude_desktop.mdx
index fd14e02bd..e31704d32 100644
--- a/docs/python-sdk/fastmcp-cli-install-claude_desktop.mdx
+++ b/docs/python-sdk/fastmcp-cli-install-claude_desktop.mdx
@@ -33,7 +33,7 @@ Install FastMCP server in Claude Desktop.
- `file`: Path to the server file
- `server_object`: Optional server object name (for \:object suffix)
- `name`: Name for the server in Claude's config
-- `with_editable`: Optional directory to install in editable mode
+- `with_editable`: Optional list of directories to install in editable mode
- `with_packages`: Optional list of additional packages to install
- `env_vars`: Optional dictionary of environment variables
- `python_version`: Optional Python version to use
diff --git a/docs/python-sdk/fastmcp-cli-install-cursor.mdx b/docs/python-sdk/fastmcp-cli-install-cursor.mdx
index c8d91ad7c..218575f86 100644
--- a/docs/python-sdk/fastmcp-cli-install-cursor.mdx
+++ b/docs/python-sdk/fastmcp-cli-install-cursor.mdx
@@ -57,7 +57,7 @@ Install FastMCP server to workspace-specific Cursor configuration.
- `server_object`: Optional server object name (for \:object suffix)
- `name`: Name for the server in Cursor
- `workspace_path`: Path to the workspace directory
-- `with_editable`: Optional directory to install in editable mode
+- `with_editable`: Optional list of directories to install in editable mode
- `with_packages`: Optional list of additional packages to install
- `env_vars`: Optional dictionary of environment variables
- `python_version`: Optional Python version to use
@@ -81,7 +81,7 @@ Install FastMCP server in Cursor.
- `file`: Path to the server file
- `server_object`: Optional server object name (for \:object suffix)
- `name`: Name for the server in Cursor
-- `with_editable`: Optional directory to install in editable mode
+- `with_editable`: Optional list of directories to install in editable mode
- `with_packages`: Optional list of additional packages to install
- `env_vars`: Optional dictionary of environment variables
- `python_version`: Optional Python version to use
diff --git a/docs/python-sdk/fastmcp-cli-install-mcp_json.mdx b/docs/python-sdk/fastmcp-cli-install-mcp_json.mdx
index fb6f2a326..0d218623e 100644
--- a/docs/python-sdk/fastmcp-cli-install-mcp_json.mdx
+++ b/docs/python-sdk/fastmcp-cli-install-mcp_json.mdx
@@ -23,7 +23,7 @@ Generate MCP configuration JSON for manual installation.
- `file`: Path to the server file
- `server_object`: Optional server object name (for \:object suffix)
- `name`: Name for the server in MCP config
-- `with_editable`: Optional directory to install in editable mode
+- `with_editable`: Optional list of directories to install in editable mode
- `with_packages`: Optional list of additional packages to install
- `env_vars`: Optional dictionary of environment variables
- `copy`: If True, copy to clipboard instead of printing to stdout
diff --git a/docs/python-sdk/fastmcp-cli-install-shared.mdx b/docs/python-sdk/fastmcp-cli-install-shared.mdx
index 8e959d21e..337179267 100644
--- a/docs/python-sdk/fastmcp-cli-install-shared.mdx
+++ b/docs/python-sdk/fastmcp-cli-install-shared.mdx
@@ -23,7 +23,7 @@ Parse environment variable string in format KEY=VALUE.
### `process_common_args`
```python
-process_common_args(server_spec: str, server_name: str | None, with_packages: list[str], env_vars: list[str], env_file: Path | None) -> tuple[Path, str | None, str, list[str], dict[str, str] | None]
+process_common_args(server_spec: str, server_name: str | None, with_packages: list[str] | None, env_vars: list[str] | None, env_file: Path | None) -> tuple[Path, str | None, str, list[str], dict[str, str] | None]
```
diff --git a/docs/python-sdk/fastmcp-cli-run.mdx b/docs/python-sdk/fastmcp-cli-run.mdx
index 74bb0b1a1..9f352a1e1 100644
--- a/docs/python-sdk/fastmcp-cli-run.mdx
+++ b/docs/python-sdk/fastmcp-cli-run.mdx
@@ -10,7 +10,7 @@ FastMCP run command implementation with enhanced type hints.
## Functions
-### `is_url`
+### `is_url`
```python
is_url(path: str) -> bool
@@ -20,43 +20,10 @@ is_url(path: str) -> bool
Check if a string is a URL.
-### `parse_file_path`
+### `run_with_uv`
```python
-parse_file_path(server_spec: str) -> tuple[Path, str | None]
-```
-
-
-Parse a file path that may include a server object specification.
-
-**Args:**
-- `server_spec`: Path to file, optionally with \:object suffix
-
-**Returns:**
-- Tuple of (file_path, server_object)
-
-
-### `import_server`
-
-```python
-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_or_factory`: Optional object name in format "module\:object" or just "object"
-
-**Returns:**
-- The server object (or result of calling a factory function)
-
-
-### `run_with_uv`
-
-```python
-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, editable: str | None = None) -> None
+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, editable: str | list[str] | None = None) -> None
```
@@ -76,7 +43,7 @@ Run a MCP server using uv run subprocess.
- `show_banner`: Whether to show the server banner
-### `create_client_server`
+### `create_client_server`
```python
create_client_server(url: str) -> Any
@@ -92,7 +59,7 @@ Create a FastMCP server from a client URL.
- A FastMCP server instance
-### `create_mcp_config_server`
+### `create_mcp_config_server`
```python
create_mcp_config_server(mcp_config_path: Path) -> FastMCP[None]
@@ -102,7 +69,7 @@ create_mcp_config_server(mcp_config_path: Path) -> FastMCP[None]
Create a FastMCP server from a MCPConfig.
-### `load_fastmcp_config`
+### `load_fastmcp_config`
```python
load_fastmcp_config(config_path: Path) -> FastMCPConfig
@@ -118,28 +85,10 @@ Load a FastMCP configuration from a fastmcp.json file.
- FastMCPConfig object
-### `import_server_with_args`
+### `run_command`
```python
-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_or_factory`: Optional server object or factory function name
-- `server_args`: Optional command line arguments to inject
-
-**Returns:**
-- The imported server object
-
-
-### `run_command`
-
-```python
-run_command(server_spec: str, transport: TransportType | None = None, host: str | None = None, port: int | None = None, path: str | None = None, log_level: LogLevelType | None = None, server_args: list[str] | None = None, show_banner: bool = True, use_direct_import: bool = False) -> None
+run_command(server_spec: str, transport: TransportType | None = None, host: str | None = None, port: int | None = None, path: str | None = None, log_level: LogLevelType | None = None, server_args: list[str] | None = None, show_banner: bool = True, use_direct_import: bool = False, skip_source: bool = False) -> None
```
@@ -155,9 +104,10 @@ Run a MCP server or connect to a remote one.
- `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
+- `skip_source`: Whether to skip source preparation step
-### `run_v1_server`
+### `run_v1_server`
```python
run_v1_server(server: FastMCP1x, host: str | None = None, port: int | None = None, transport: TransportType | None = None) -> None
diff --git a/docs/python-sdk/fastmcp-server-auth-oauth_proxy.mdx b/docs/python-sdk/fastmcp-server-auth-oauth_proxy.mdx
index 84b2d4123..b592bdc8c 100644
--- a/docs/python-sdk/fastmcp-server-auth-oauth_proxy.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-oauth_proxy.mdx
@@ -226,7 +226,7 @@ The flow:
4. When client reconnects with a different port, ProxyDCRClient accepts it
-#### `authorize`
+#### `authorize`
```python
authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str
@@ -240,7 +240,7 @@ This implements the DCR-compliant proxy pattern:
3. Redirect to IdP with our fixed callback URL
-#### `load_authorization_code`
+#### `load_authorization_code`
```python
load_authorization_code(self, client: OAuthClientInformationFull, authorization_code: str) -> AuthorizationCode | None
@@ -252,7 +252,7 @@ Look up our client code and return authorization code object
with PKCE challenge for validation.
-#### `exchange_authorization_code`
+#### `exchange_authorization_code`
```python
exchange_authorization_code(self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode) -> OAuthToken
@@ -264,7 +264,7 @@ For the DCR-compliant proxy flow, we return the IdP tokens that were obtained
during the IdP callback exchange. PKCE validation is handled by the MCP framework.
-#### `load_refresh_token`
+#### `load_refresh_token`
```python
load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None
@@ -273,7 +273,7 @@ load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str)
Load refresh token from local storage.
-#### `exchange_refresh_token`
+#### `exchange_refresh_token`
```python
exchange_refresh_token(self, client: OAuthClientInformationFull, refresh_token: RefreshToken, scopes: list[str]) -> OAuthToken
@@ -282,7 +282,7 @@ exchange_refresh_token(self, client: OAuthClientInformationFull, refresh_token:
Exchange refresh token for new access token using authlib.
-#### `load_access_token`
+#### `load_access_token`
```python
load_access_token(self, token: str) -> AccessToken | None
@@ -294,7 +294,7 @@ Delegates to the JWT verifier which handles signature validation,
expiration checking, and claims validation using the upstream JWKS.
-#### `revoke_token`
+#### `revoke_token`
```python
revoke_token(self, token: AccessToken | RefreshToken) -> None
@@ -306,7 +306,7 @@ Removes tokens from local storage and attempts to revoke them with
the upstream server if a revocation endpoint is configured.
-#### `get_routes`
+#### `get_routes`
```python
get_routes(self) -> list[Route]
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx b/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx
index 559f1238a..ed66062f9 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx
@@ -14,13 +14,13 @@ using the OAuth Proxy pattern for non-DCR OAuth flows.
## Classes
-### `AzureProviderSettings`
+### `AzureProviderSettings`
Settings for Azure OAuth provider.
-### `AzureTokenVerifier`
+### `AzureTokenVerifier`
Token verifier for Azure OAuth tokens.
@@ -31,7 +31,7 @@ to get user information and validate the token.
**Methods:**
-#### `verify_token`
+#### `verify_token`
```python
verify_token(self, token: str) -> AccessToken | None
@@ -40,7 +40,7 @@ verify_token(self, token: str) -> AccessToken | None
Verify Azure OAuth token by calling Microsoft Graph API.
-### `AzureProvider`
+### `AzureProvider`
Azure (Microsoft Entra) OAuth provider for FastMCP.
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-github.mdx b/docs/python-sdk/fastmcp-server-auth-providers-github.mdx
index 72273c01a..e6f23ab15 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-github.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-github.mdx
@@ -29,13 +29,13 @@ Example:
## Classes
-### `GitHubProviderSettings`
+### `GitHubProviderSettings`
Settings for GitHub OAuth provider.
-### `GitHubTokenVerifier`
+### `GitHubTokenVerifier`
Token verifier for GitHub OAuth tokens.
@@ -46,7 +46,7 @@ by calling GitHub's API to check if they're valid and get user info.
**Methods:**
-#### `verify_token`
+#### `verify_token`
```python
verify_token(self, token: str) -> AccessToken | None
@@ -55,7 +55,7 @@ verify_token(self, token: str) -> AccessToken | None
Verify GitHub OAuth token by calling GitHub API.
-### `GitHubProvider`
+### `GitHubProvider`
Complete GitHub OAuth provider for FastMCP.
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-google.mdx b/docs/python-sdk/fastmcp-server-auth-providers-google.mdx
index f7bbd5ee0..4f4d4688b 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-google.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-google.mdx
@@ -29,13 +29,13 @@ Example:
## Classes
-### `GoogleProviderSettings`
+### `GoogleProviderSettings`
Settings for Google OAuth provider.
-### `GoogleTokenVerifier`
+### `GoogleTokenVerifier`
Token verifier for Google OAuth tokens.
@@ -46,7 +46,7 @@ by calling Google's tokeninfo API to check if they're valid and get user info.
**Methods:**
-#### `verify_token`
+#### `verify_token`
```python
verify_token(self, token: str) -> AccessToken | None
@@ -55,7 +55,7 @@ verify_token(self, token: str) -> AccessToken | None
Verify Google OAuth token by calling Google's tokeninfo API.
-### `GoogleProvider`
+### `GoogleProvider`
Complete Google OAuth provider for FastMCP.
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-jwt.mdx b/docs/python-sdk/fastmcp-server-auth-providers-jwt.mdx
index b8af2f930..1c7224bd0 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-jwt.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-jwt.mdx
@@ -10,19 +10,19 @@ TokenVerifier implementations for FastMCP.
## Classes
-### `JWKData`
+### `JWKData`
JSON Web Key data structure.
-### `JWKSData`
+### `JWKSData`
JSON Web Key Set data structure.
-### `RSAKeyPair`
+### `RSAKeyPair`
RSA key pair for JWT testing.
@@ -30,7 +30,7 @@ RSA key pair for JWT testing.
**Methods:**
-#### `generate`
+#### `generate`
```python
generate(cls) -> RSAKeyPair
@@ -42,7 +42,7 @@ Generate an RSA key pair for testing.
- Generated key pair
-#### `create_token`
+#### `create_token`
```python
create_token(self, subject: str = 'fastmcp-user', issuer: str = 'https://fastmcp.example.com', audience: str | list[str] | None = None, scopes: list[str] | None = None, expires_in_seconds: int = 3600, additional_claims: dict[str, Any] | None = None, kid: str | None = None) -> str
@@ -60,13 +60,13 @@ Generate a test JWT token for testing purposes.
- `kid`: Key ID to include in header
-### `JWTVerifierSettings`
+### `JWTVerifierSettings`
Settings for JWT token verification.
-### `JWTVerifier`
+### `JWTVerifier`
JWT token verifier supporting both asymmetric (RSA/ECDSA) and symmetric (HMAC) algorithms.
@@ -88,7 +88,7 @@ Use this when:
**Methods:**
-#### `load_access_token`
+#### `load_access_token`
```python
load_access_token(self, token: str) -> AccessToken | None
@@ -103,7 +103,7 @@ Validates the provided JWT bearer token.
- AccessToken object if valid, None if invalid or expired
-#### `verify_token`
+#### `verify_token`
```python
verify_token(self, token: str) -> AccessToken | None
@@ -121,7 +121,7 @@ to our existing load_access_token method.
- AccessToken object if valid, None if invalid or expired
-### `StaticTokenVerifier`
+### `StaticTokenVerifier`
Simple static token verifier for testing and development.
@@ -142,7 +142,7 @@ WARNING: Never use this in production - tokens are stored in plain text!
**Methods:**
-#### `verify_token`
+#### `verify_token`
```python
verify_token(self, token: str) -> AccessToken | None
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx b/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx
index f391c4307..60781bf24 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx
@@ -18,13 +18,13 @@ Choose based on your WorkOS setup and authentication requirements.
## Classes
-### `WorkOSProviderSettings`
+### `WorkOSProviderSettings`
Settings for WorkOS OAuth provider.
-### `WorkOSTokenVerifier`
+### `WorkOSTokenVerifier`
Token verifier for WorkOS OAuth tokens.
@@ -35,7 +35,7 @@ the /oauth2/userinfo endpoint to check validity and get user info.
**Methods:**
-#### `verify_token`
+#### `verify_token`
```python
verify_token(self, token: str) -> AccessToken | None
@@ -44,7 +44,7 @@ verify_token(self, token: str) -> AccessToken | None
Verify WorkOS OAuth token by calling userinfo endpoint.
-### `WorkOSProvider`
+### `WorkOSProvider`
Complete WorkOS OAuth provider for FastMCP.
@@ -65,9 +65,9 @@ Setup Requirements:
4. Note your Client ID and Client Secret
-### `AuthKitProviderSettings`
+### `AuthKitProviderSettings`
-### `AuthKitProvider`
+### `AuthKitProvider`
AuthKit metadata provider for DCR (Dynamic Client Registration).
@@ -93,7 +93,7 @@ https://workos.com/docs/authkit/mcp/integrating/token-verification
**Methods:**
-#### `get_routes`
+#### `get_routes`
```python
get_routes(self) -> list[Route]
diff --git a/docs/python-sdk/fastmcp-server-elicitation.mdx b/docs/python-sdk/fastmcp-server-elicitation.mdx
index f3878efd7..b836cf49a 100644
--- a/docs/python-sdk/fastmcp-server-elicitation.mdx
+++ b/docs/python-sdk/fastmcp-server-elicitation.mdx
@@ -7,7 +7,7 @@ sidebarTitle: elicitation
## Functions
-### `get_elicitation_schema`
+### `get_elicitation_schema`
```python
get_elicitation_schema(response_type: type[T]) -> dict[str, Any]
@@ -20,7 +20,7 @@ Get the schema for an elicitation response.
- `response_type`: The type of the response
-### `validate_elicitation_json_schema`
+### `validate_elicitation_json_schema`
```python
validate_elicitation_json_schema(schema: dict[str, Any]) -> None
@@ -45,10 +45,43 @@ This ensures the schema is compatible with MCP elicitation requirements:
## Classes
-### `AcceptedElicitation`
+### `ElicitationJsonSchema`
+
+
+Custom JSON schema generator for MCP elicitation that always inlines enums.
+
+MCP elicitation requires inline enum schemas without $ref/$defs references.
+This generator ensures enums are always generated inline for compatibility.
+Optionally adds enumNames for better UI display when available.
+
+
+**Methods:**
+
+#### `generate_inner`
+
+```python
+generate_inner(self, schema: core_schema.CoreSchema) -> JsonSchemaValue
+```
+
+Override to prevent ref generation for enums.
+
+
+#### `enum_schema`
+
+```python
+enum_schema(self, schema: core_schema.EnumSchema) -> JsonSchemaValue
+```
+
+Generate inline enum schema with optional enumNames for better UI.
+
+If enum members have a _display_name_ attribute or custom __str__,
+we'll include enumNames for better UI representation.
+
+
+### `AcceptedElicitation`
Result when user accepts the elicitation.
-### `ScalarElicitationType`
+### `ScalarElicitationType`
diff --git a/docs/python-sdk/fastmcp-tools-tool_transform.mdx b/docs/python-sdk/fastmcp-tools-tool_transform.mdx
index fe16ae521..9ad99ae30 100644
--- a/docs/python-sdk/fastmcp-tools-tool_transform.mdx
+++ b/docs/python-sdk/fastmcp-tools-tool_transform.mdx
@@ -62,7 +62,7 @@ y=2)` will call the parent tool with `x=1` and `y=2`.
- `RuntimeError`: If called outside a transformed tool context.
-### `apply_transformations_to_tools`
+### `apply_transformations_to_tools`
```python
apply_transformations_to_tools(tools: dict[str, Tool], transformations: dict[str, ToolTransformConfig]) -> dict[str, Tool]
@@ -272,7 +272,7 @@ async def custom_output(**kwargs) -> ToolResult:
```
-### `ToolTransformConfig`
+### `ToolTransformConfig`
Provides a way to transform a tool.
@@ -280,7 +280,7 @@ Provides a way to transform a tool.
**Methods:**
-#### `apply`
+#### `apply`
```python
apply(self, tool: Tool) -> TransformedTool
diff --git a/docs/python-sdk/fastmcp-utilities-auth.mdx b/docs/python-sdk/fastmcp-utilities-auth.mdx
new file mode 100644
index 000000000..f30966e96
--- /dev/null
+++ b/docs/python-sdk/fastmcp-utilities-auth.mdx
@@ -0,0 +1,25 @@
+---
+title: auth
+sidebarTitle: auth
+---
+
+# `fastmcp.utilities.auth`
+
+
+Authentication utility helpers.
+
+## Functions
+
+### `parse_scopes`
+
+```python
+parse_scopes(value: Any) -> list[str] | None
+```
+
+
+Parse scopes from environment variables or settings values.
+
+Accepts either a JSON array string, a comma- or space-separated string,
+a list of strings, or ``None``. Returns a list of scopes or ``None`` if
+no value is provided.
+
diff --git a/docs/python-sdk/fastmcp-utilities-fastmcp_config.mdx b/docs/python-sdk/fastmcp-utilities-fastmcp_config-__init__.mdx
similarity index 81%
rename from docs/python-sdk/fastmcp-utilities-fastmcp_config.mdx
rename to docs/python-sdk/fastmcp-utilities-fastmcp_config-__init__.mdx
index 1377f6eeb..f804aa19f 100644
--- a/docs/python-sdk/fastmcp-utilities-fastmcp_config.mdx
+++ b/docs/python-sdk/fastmcp-utilities-fastmcp_config-__init__.mdx
@@ -1,6 +1,6 @@
---
-title: fastmcp_config
-sidebarTitle: fastmcp_config
+title: __init__
+sidebarTitle: __init__
---
# `fastmcp.utilities.fastmcp_config`
diff --git a/docs/python-sdk/fastmcp-utilities-fastmcp_config-v1-__init__.mdx b/docs/python-sdk/fastmcp-utilities-fastmcp_config-v1-__init__.mdx
new file mode 100644
index 000000000..4195f2dc2
--- /dev/null
+++ b/docs/python-sdk/fastmcp-utilities-fastmcp_config-v1-__init__.mdx
@@ -0,0 +1,8 @@
+---
+title: __init__
+sidebarTitle: __init__
+---
+
+# `fastmcp.utilities.fastmcp_config.v1`
+
+*This module is empty or contains only private/internal implementations.*
diff --git a/docs/python-sdk/fastmcp-utilities-fastmcp_config-v1-fastmcp_config.mdx b/docs/python-sdk/fastmcp-utilities-fastmcp_config-v1-fastmcp_config.mdx
new file mode 100644
index 000000000..d66382bc1
--- /dev/null
+++ b/docs/python-sdk/fastmcp-utilities-fastmcp_config-v1-fastmcp_config.mdx
@@ -0,0 +1,297 @@
+---
+title: fastmcp_config
+sidebarTitle: fastmcp_config
+---
+
+# `fastmcp.utilities.fastmcp_config.v1.fastmcp_config`
+
+
+FastMCP Configuration File Support.
+
+This module provides support for fastmcp.json configuration files that allow
+users to specify server settings in a declarative format instead of using
+command-line arguments.
+
+
+## Functions
+
+### `generate_schema`
+
+```python
+generate_schema(output_path: Path | str | None = None) -> dict[str, Any] | None
+```
+
+
+Generate JSON schema for fastmcp.json files.
+
+This is used to create the schema file that IDEs can use for
+validation and auto-completion.
+
+**Args:**
+- `output_path`: Optional path to write the schema to. If provided,
+ writes the schema and returns None. If not provided,
+ returns the schema as a dictionary.
+
+**Returns:**
+- JSON schema as a dictionary if output_path is None, otherwise None
+
+
+## Classes
+
+### `Environment`
+
+
+Configuration for Python environment setup.
+
+
+**Methods:**
+
+#### `build_uv_args`
+
+```python
+build_uv_args(self, command: str | list[str] | None = None) -> list[str]
+```
+
+Build uv run arguments from this environment configuration.
+
+**Args:**
+- `command`: Optional command to append (string or list of args)
+
+**Returns:**
+- List of arguments for uv run command
+
+
+#### `run_with_uv`
+
+```python
+run_with_uv(self, command: list[str]) -> None
+```
+
+Execute a command using uv run with this environment configuration.
+
+**Args:**
+- `command`: Command and arguments to execute (e.g., ["fastmcp", "run", "server.py"])
+
+
+#### `needs_uv`
+
+```python
+needs_uv(self) -> bool
+```
+
+Check if this environment config requires uv to set up.
+
+**Returns:**
+- True if any environment settings require uv run
+
+
+#### `prepare`
+
+```python
+prepare(self, output_dir: Path | None = None) -> None
+```
+
+Prepare the Python environment using uv.
+
+**Args:**
+- `output_dir`: Directory where the persistent uv project will be created.
+ If None, creates a temporary directory for ephemeral use.
+
+
+### `Deployment`
+
+
+Configuration for server deployment and runtime settings.
+
+
+**Methods:**
+
+#### `apply_runtime_settings`
+
+```python
+apply_runtime_settings(self, config_path: Path | None = None) -> None
+```
+
+Apply runtime settings like environment variables and working directory.
+
+**Args:**
+- `config_path`: Path to config file for resolving relative paths
+
+Environment variables support interpolation with ${VAR_NAME} syntax.
+For example: "API_URL": "https://api.${ENVIRONMENT}.example.com"
+will substitute the value of the ENVIRONMENT variable at runtime.
+
+
+### `FastMCPConfig`
+
+
+Configuration for a FastMCP server.
+
+This configuration file allows you to specify all settings needed to run
+a FastMCP server in a declarative format.
+
+
+**Methods:**
+
+#### `validate_source`
+
+```python
+validate_source(cls, v: dict | FileSystemSource) -> FileSystemSource
+```
+
+Validate and convert source to proper format.
+
+Supports:
+- Dict format: {"path": "server.py", "entrypoint": "app"}
+- FileSystemSource instance (passed through)
+
+No string parsing happens here - that's only at CLI boundaries.
+FastMCPConfig works only with properly typed objects.
+
+
+#### `validate_environment`
+
+```python
+validate_environment(cls, v: dict | Environment) -> Environment
+```
+
+Validate and convert environment to Environment.
+
+Accepts:
+- Environment instance
+- dict that can be converted to Environment
+
+
+#### `validate_deployment`
+
+```python
+validate_deployment(cls, v: dict | Deployment) -> Deployment
+```
+
+Validate and convert deployment to Deployment.
+
+Accepts:
+- Deployment instance
+- dict that can be converted to Deployment
+
+
+#### `from_file`
+
+```python
+from_file(cls, file_path: Path) -> FastMCPConfig
+```
+
+Load configuration from a JSON file.
+
+**Args:**
+- `file_path`: Path to the configuration file
+
+**Returns:**
+- FastMCPConfig instance
+
+**Raises:**
+- `FileNotFoundError`: If the file doesn't exist
+- `json.JSONDecodeError`: If the file is not valid JSON
+- `pydantic.ValidationError`: If the configuration is invalid
+
+
+#### `from_cli_args`
+
+```python
+from_cli_args(cls, source: FileSystemSource, transport: Literal['stdio', 'http', 'sse', 'streamable-http'] | None = None, host: str | None = None, port: int | None = None, path: str | None = None, log_level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] | None = None, python: str | None = None, dependencies: list[str] | None = None, requirements: str | None = None, project: str | None = None, editable: str | None = None, env: dict[str, str] | None = None, cwd: str | None = None, args: list[str] | None = None) -> FastMCPConfig
+```
+
+Create a config from CLI arguments.
+
+This allows us to have a single code path where everything
+goes through a config object.
+
+**Args:**
+- `source`: Server source (FileSystemSource instance)
+- `transport`: Transport protocol
+- `host`: Host for HTTP transport
+- `port`: Port for HTTP transport
+- `path`: URL path for server
+- `log_level`: Logging level
+- `python`: Python version
+- `dependencies`: Python packages to install
+- `requirements`: Path to requirements file
+- `project`: Path to project directory
+- `editable`: Path to install in editable mode
+- `env`: Environment variables
+- `cwd`: Working directory
+- `args`: Server arguments
+
+**Returns:**
+- FastMCPConfig instance
+
+
+#### `find_config`
+
+```python
+find_config(cls, start_path: Path | None = None) -> Path | None
+```
+
+Find a fastmcp.json file in the specified directory.
+
+**Args:**
+- `start_path`: Directory to look in (defaults to current directory)
+
+**Returns:**
+- Path to the configuration file, or None if not found
+
+
+#### `prepare`
+
+```python
+prepare(self, skip_source: bool = False, output_dir: Path | None = None) -> None
+```
+
+Prepare environment and source for execution.
+
+When output_dir is provided, creates a persistent uv project.
+When output_dir is None, does ephemeral caching (for backwards compatibility).
+
+**Args:**
+- `skip_source`: Skip source preparation if True
+- `output_dir`: Directory to create the persistent uv project in (optional)
+
+
+#### `prepare_environment`
+
+```python
+prepare_environment(self, output_dir: Path | None = None) -> None
+```
+
+Prepare the Python environment.
+
+**Args:**
+- `output_dir`: If provided, creates a persistent uv project in this directory.
+ If None, just populates uv's cache for ephemeral use.
+
+Delegates to the environment's prepare() method
+
+
+#### `prepare_source`
+
+```python
+prepare_source(self) -> None
+```
+
+Prepare the source for loading.
+
+Delegates to the source's prepare() method.
+
+
+#### `run_server`
+
+```python
+run_server(self, **kwargs: Any) -> None
+```
+
+Load and run the server with this configuration.
+
+**Args:**
+- `**kwargs`: Additional arguments to pass to server.run_async()
+ These override config settings
+
diff --git a/docs/python-sdk/fastmcp-utilities-fastmcp_config-v1-sources-__init__.mdx b/docs/python-sdk/fastmcp-utilities-fastmcp_config-v1-sources-__init__.mdx
new file mode 100644
index 000000000..c7259c5a1
--- /dev/null
+++ b/docs/python-sdk/fastmcp-utilities-fastmcp_config-v1-sources-__init__.mdx
@@ -0,0 +1,8 @@
+---
+title: __init__
+sidebarTitle: __init__
+---
+
+# `fastmcp.utilities.fastmcp_config.v1.sources`
+
+*This module is empty or contains only private/internal implementations.*
diff --git a/docs/python-sdk/fastmcp-utilities-fastmcp_config-v1-sources-base.mdx b/docs/python-sdk/fastmcp-utilities-fastmcp_config-v1-sources-base.mdx
new file mode 100644
index 000000000..15d2876f7
--- /dev/null
+++ b/docs/python-sdk/fastmcp-utilities-fastmcp_config-v1-sources-base.mdx
@@ -0,0 +1,42 @@
+---
+title: base
+sidebarTitle: base
+---
+
+# `fastmcp.utilities.fastmcp_config.v1.sources.base`
+
+## Classes
+
+### `BaseSource`
+
+
+Abstract base class for all source types.
+
+
+**Methods:**
+
+#### `prepare`
+
+```python
+prepare(self) -> None
+```
+
+Prepare the source (download, clone, install, etc).
+
+For sources that need preparation (e.g., git clone, download),
+this method performs that preparation. For sources that don't
+need preparation (e.g., local files), this is a no-op.
+
+
+#### `load_server`
+
+```python
+load_server(self) -> Any
+```
+
+Load and return the FastMCP server instance.
+
+Must be called after prepare() if the source requires preparation.
+All information needed to load the server should be available
+as attributes on the source instance.
+
diff --git a/docs/python-sdk/fastmcp-utilities-fastmcp_config-v1-sources-filesystem.mdx b/docs/python-sdk/fastmcp-utilities-fastmcp_config-v1-sources-filesystem.mdx
new file mode 100644
index 000000000..68f3bd6fb
--- /dev/null
+++ b/docs/python-sdk/fastmcp-utilities-fastmcp_config-v1-sources-filesystem.mdx
@@ -0,0 +1,37 @@
+---
+title: filesystem
+sidebarTitle: filesystem
+---
+
+# `fastmcp.utilities.fastmcp_config.v1.sources.filesystem`
+
+## Classes
+
+### `FileSystemSource`
+
+
+Source for local Python files.
+
+
+**Methods:**
+
+#### `parse_path_with_object`
+
+```python
+parse_path_with_object(cls, v: str) -> str
+```
+
+Parse path:object syntax and extract the object name.
+
+This validator runs before the model is created, allowing us to
+handle the "file.py:object" syntax at the model boundary.
+
+
+#### `load_server`
+
+```python
+load_server(self) -> Any
+```
+
+Load server from filesystem.
+
diff --git a/docs/python-sdk/fastmcp-utilities-inspect.mdx b/docs/python-sdk/fastmcp-utilities-inspect.mdx
index d05ddcff5..b48e52a5c 100644
--- a/docs/python-sdk/fastmcp-utilities-inspect.mdx
+++ b/docs/python-sdk/fastmcp-utilities-inspect.mdx
@@ -10,7 +10,7 @@ Utilities for inspecting FastMCP instances.
## Functions
-### `inspect_fastmcp_v2`
+### `inspect_fastmcp_v2`
```python
inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo
@@ -26,7 +26,7 @@ Extract information from a FastMCP v2.x instance.
- FastMCPInfo dataclass containing the extracted information
-### `inspect_fastmcp_v1`
+### `inspect_fastmcp_v1`
```python
inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo
@@ -42,7 +42,7 @@ Extract information from a FastMCP v1.x instance using a Client.
- FastMCPInfo dataclass containing the extracted information
-### `inspect_fastmcp`
+### `inspect_fastmcp`
```python
inspect_fastmcp(mcp: FastMCP[Any] | FastMCP1x) -> FastMCPInfo
@@ -61,34 +61,83 @@ and uses the appropriate extraction method.
- FastMCPInfo dataclass containing the extracted information
+### `format_fastmcp_info`
+
+```python
+format_fastmcp_info(info: FastMCPInfo) -> bytes
+```
+
+
+Format FastMCPInfo as FastMCP-specific JSON.
+
+This includes FastMCP-specific fields like tags, enabled, annotations, etc.
+
+
+### `format_mcp_info`
+
+```python
+format_mcp_info(mcp: FastMCP[Any] | FastMCP1x) -> bytes
+```
+
+
+Format server info as standard MCP protocol JSON.
+
+Uses Client to get the standard MCP protocol format with camelCase fields.
+Includes version metadata at the top level.
+
+
+### `format_info`
+
+```python
+format_info(mcp: FastMCP[Any] | FastMCP1x, format: InspectFormat | Literal['fastmcp', 'mcp'], info: FastMCPInfo | None = None) -> bytes
+```
+
+
+Format server information according to the specified format.
+
+**Args:**
+- `mcp`: The FastMCP instance
+- `format`: Output format ("fastmcp" or "mcp")
+- `info`: Pre-extracted FastMCPInfo (optional, will be extracted if not provided)
+
+**Returns:**
+- JSON bytes in the requested format
+
+
## Classes
-### `ToolInfo`
+### `ToolInfo`
Information about a tool.
-### `PromptInfo`
+### `PromptInfo`
Information about a prompt.
-### `ResourceInfo`
+### `ResourceInfo`
Information about a resource.
-### `TemplateInfo`
+### `TemplateInfo`
Information about a resource template.
-### `FastMCPInfo`
+### `FastMCPInfo`
Information extracted from a FastMCP instance.
+
+### `InspectFormat`
+
+
+Output format for inspect command.
+
diff --git a/src/fastmcp/cli/claude.py b/src/fastmcp/cli/claude.py
index fa4d529de..8873a3a7a 100644
--- a/src/fastmcp/cli/claude.py
+++ b/src/fastmcp/cli/claude.py
@@ -34,7 +34,7 @@ def update_claude_config(
file_spec: str,
server_name: str,
*,
- with_editable: Path | None = None,
+ with_editable: list[Path] | None = None,
with_packages: list[str] | None = None,
env_vars: dict[str, str] | None = None,
) -> bool:
@@ -43,7 +43,7 @@ def update_claude_config(
Args:
file_spec: Path to the server file, optionally with :object suffix
server_name: Name for the server in Claude's config
- with_editable: Optional directory to install in editable mode
+ with_editable: Optional list of directories to install in editable mode
with_packages: Optional list of additional packages to install
env_vars: Optional dictionary of environment variables. These are merged with
any existing variables, with new values taking precedence.
@@ -101,7 +101,7 @@ def update_claude_config(
# Build uv run command using Environment.build_uv_args()
env_config = Environment(
dependencies=deduplicated_packages,
- editable=[str(with_editable)] if with_editable else None,
+ editable=[str(p) for p in with_editable] if with_editable else None,
)
args = env_config.build_uv_args()
diff --git a/src/fastmcp/cli/cli.py b/src/fastmcp/cli/cli.py
index aba034e20..34e1a908c 100644
--- a/src/fastmcp/cli/cli.py
+++ b/src/fastmcp/cli/cli.py
@@ -13,7 +13,7 @@ from typing import Annotated, Literal
import cyclopts
import pyperclip
-from pydantic import TypeAdapter, ValidationError
+from pydantic import ValidationError
from rich.console import Console
from rich.table import Table
@@ -21,7 +21,13 @@ import fastmcp
from fastmcp.cli import run as run_module
from fastmcp.cli.install import install_app
from fastmcp.server.server import FastMCP
-from fastmcp.utilities.inspect import FastMCPInfo, inspect_fastmcp
+from fastmcp.utilities.fastmcp_config import Environment, FastMCPConfig
+from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource
+from fastmcp.utilities.inspect import (
+ InspectFormat,
+ format_info,
+ inspect_fastmcp,
+)
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import get_cached_typeadapter
@@ -126,20 +132,21 @@ async def dev(
server_spec: str | None = None,
*,
with_editable: Annotated[
- Path | None,
+ list[Path] | None,
cyclopts.Parameter(
- name=["--with-editable", "-e"],
- help="Directory containing pyproject.toml to install in editable mode",
+ "--with-editable",
+ help="Directory containing pyproject.toml to install in editable mode (can be used multiple times)",
+ negative="",
),
] = None,
with_packages: Annotated[
- list[str],
+ list[str] | None,
cyclopts.Parameter(
"--with",
- help="Additional packages to install",
+ help="Additional packages to install (can be used multiple times)",
negative="",
),
- ] = [],
+ ] = None,
inspector_version: Annotated[
str | None,
cyclopts.Parameter(
@@ -188,6 +195,9 @@ async def dev(
Args:
server_spec: Python file to run, optionally with :object suffix, or None to auto-detect fastmcp.json
"""
+ # Convert None to empty lists for list parameters
+ with_editable = with_editable or []
+ with_packages = with_packages or []
from pathlib import Path
from fastmcp.utilities.fastmcp_config import FastMCPConfig
@@ -229,13 +239,9 @@ async def dev(
if config.environment.requirements
else None
)
- # Note: config.environment.editable is a list, but CLI only supports single path
- # Take the first editable path if available
- with_editable = with_editable or (
- Path(config.environment.editable[0])
- if config.environment.editable and config.environment.editable[0]
- else None
- )
+ # Merge editable paths from config with CLI args
+ if config.environment.editable and not with_editable:
+ with_editable = [Path(p) for p in config.environment.editable]
# Merge packages from both sources
if config.environment.dependencies:
@@ -256,7 +262,7 @@ async def dev(
"Starting dev server",
extra={
"server_spec": server_spec,
- "with_editable": str(with_editable) if with_editable else None,
+ "with_editable": [str(p) for p in with_editable] if with_editable else None,
"with_packages": with_packages,
"ui_port": ui_port,
"server_port": server_port,
@@ -298,14 +304,12 @@ async def dev(
inspector_cmd += f"@{inspector_version}"
# Create Environment object from CLI args
- from fastmcp.utilities.fastmcp_config import Environment
-
env_config = Environment(
python=python,
dependencies=with_packages if with_packages else None,
requirements=str(with_requirements) if with_requirements else None,
project=str(project) if project else None,
- editable=[str(with_editable)] if with_editable else None,
+ editable=[str(p) for p in with_editable] if with_editable else None,
)
uv_cmd = ["uv"] + env_config.build_uv_args(["fastmcp", "run", server_spec])
@@ -396,13 +400,13 @@ async def run(
),
] = None,
with_packages: Annotated[
- list[str],
+ list[str] | None,
cyclopts.Parameter(
"--with",
help="Additional packages to install (can be used multiple times)",
negative="",
),
- ] = [],
+ ] = None,
project: Annotated[
Path | None,
cyclopts.Parameter(
@@ -450,6 +454,8 @@ async def run(
Args:
server_spec: Python file, object specification (file:obj), config file, URL, or None to auto-detect
"""
+ # Convert None to empty lists for list parameters
+ with_packages = with_packages or []
# Load configuration if needed
from pathlib import Path
@@ -616,13 +622,20 @@ async def run(
async def inspect(
server_spec: str | None = None,
*,
+ format: Annotated[
+ InspectFormat | None,
+ cyclopts.Parameter(
+ name=["--format", "-f"],
+ help="Output format: fastmcp (FastMCP-specific) or mcp (MCP protocol). Required when using -o.",
+ ),
+ ] = None,
output: Annotated[
- Path,
+ Path | None,
cyclopts.Parameter(
name=["--output", "-o"],
- help="Output file path for the JSON report (default: server-info.json)",
+ help="Output file path for the JSON report. If not specified, outputs to stdout when format is provided.",
),
- ] = Path("server-info.json"),
+ ] = None,
python: Annotated[
str | None,
cyclopts.Parameter(
@@ -631,13 +644,13 @@ async def inspect(
),
] = None,
with_packages: Annotated[
- list[str],
+ list[str] | None,
cyclopts.Parameter(
"--with",
help="Additional packages to install (can be used multiple times)",
negative="",
),
- ] = [],
+ ] = None,
project: Annotated[
Path | None,
cyclopts.Parameter(
@@ -652,29 +665,39 @@ async def inspect(
help="Requirements file to install dependencies from",
),
] = None,
+ skip_env: Annotated[
+ bool,
+ cyclopts.Parameter(
+ "--skip-env",
+ help="Skip environment configuration (for internal use when already in a uv environment)",
+ negative="",
+ ),
+ ] = False,
) -> None:
- """Inspect an MCP server and generate a JSON report.
+ """Inspect an MCP server and display information or generate a JSON report.
- This command analyzes an MCP server and generates a comprehensive JSON report
- containing information about the server's name, instructions, version, tools,
- prompts, resources, templates, and capabilities.
+ This command analyzes an MCP server. Without flags, it displays a text summary.
+ Use --format to output complete JSON data.
Examples:
+ # Show text summary
fastmcp inspect server.py
- fastmcp inspect server.py -o report.json
- fastmcp inspect server.py:mcp -o analysis.json
- fastmcp inspect path/to/server.py:app -o /tmp/server-info.json
+
+ # Output FastMCP format JSON to stdout
+ fastmcp inspect server.py --format fastmcp
+
+ # Save MCP protocol format to file (format required with -o)
+ fastmcp inspect server.py --format mcp -o manifest.json
+
+ # Inspect from fastmcp.json configuration
fastmcp inspect fastmcp.json
fastmcp inspect # auto-detect fastmcp.json
Args:
server_spec: Python file to inspect, optionally with :object suffix, or fastmcp.json
"""
- from pathlib import Path
-
- from fastmcp.utilities.fastmcp_config import FastMCPConfig
- from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource
-
+ # Convert None to empty lists for list parameters
+ with_packages = with_packages or []
config = None
config_path = None
@@ -744,14 +767,15 @@ async def inspect(
source = FileSystemSource(path=server_spec)
config = FastMCPConfig(source=source)
- # Check if we need to use uv run
- needs_uv = python or with_packages or with_requirements or project
- if not needs_uv and config and config.environment:
- needs_uv = config.environment.needs_uv()
+ # Check if we need to use uv run (skip if --skip-env is set)
+ needs_uv = False
+ if not skip_env:
+ needs_uv = python or with_packages or with_requirements or project
+ if not needs_uv and config and config.environment:
+ needs_uv = config.environment.needs_uv()
if needs_uv:
# Build and run uv command
- from fastmcp.utilities.fastmcp_config import Environment
# Create or update environment config
env_config = Environment(
@@ -765,9 +789,14 @@ async def inspect(
"fastmcp",
"inspect",
server_spec,
- "--output",
- str(output),
+ "--skip-env", # Prevent infinite loop when calling through uv
]
+
+ # Add format and output flags if specified
+ if format:
+ inspect_command.extend(["--format", format.value])
+ if output:
+ inspect_command.extend(["--output", str(output)])
env_config.run_with_uv(inspect_command)
return # run_with_uv exits the process
@@ -775,7 +804,8 @@ async def inspect(
"Inspecting server",
extra={
"server_spec": server_spec,
- "output": str(output),
+ "format": format,
+ "output": str(output) if output else None,
},
)
@@ -783,29 +813,76 @@ async def inspect(
# Load the server using the config
server = await config.source.load_server()
- # Get server information - using native async support
+ # Get basic server information
info = await inspect_fastmcp(server)
- info_json = TypeAdapter(FastMCPInfo).dump_json(info, indent=2)
+ # Check for invalid combination
+ if output and not format:
+ console.print(
+ "[bold red]Error:[/bold red] --format is required when using -o/--output"
+ )
+ console.print(
+ "[dim]Use --format fastmcp or --format mcp to specify the output format[/dim]"
+ )
+ sys.exit(1)
- # Ensure output directory exists
- output.parent.mkdir(parents=True, exist_ok=True)
+ # If no format specified, show text summary
+ if format is None:
+ # Display text summary
+ console.print()
- # Write JSON report (always pretty-printed)
- with output.open("w", encoding="utf-8") as f:
- f.write(info_json.decode("utf-8"))
+ # Server section
+ console.print("[bold]Server[/bold]")
+ console.print(f" Name: {info.name}")
+ if info.version:
+ console.print(f" Version: {info.version}")
+ console.print(f" Generation: {info.server_generation}")
+ if info.instructions:
+ console.print(f" Instructions: {info.instructions}")
+ console.print()
- logger.info(f"Server inspection complete. Report saved to {output}")
+ # Components section
+ console.print("[bold]Components[/bold]")
+ console.print(f" Tools: {len(info.tools)}")
+ console.print(f" Prompts: {len(info.prompts)}")
+ console.print(f" Resources: {len(info.resources)}")
+ console.print(f" Templates: {len(info.templates)}")
+ console.print()
- # Print summary to console
- console.print(
- f"[bold green]✓[/bold green] Inspected server: [bold]{info.name}[/bold]"
- )
- console.print(f" Tools: {len(info.tools)}")
- console.print(f" Prompts: {len(info.prompts)}")
- console.print(f" Resources: {len(info.resources)}")
- console.print(f" Templates: {len(info.templates)}")
- console.print(f" Report saved to: [cyan]{output}[/cyan]")
+ # Environment section
+ console.print("[bold]Environment[/bold]")
+ console.print(f" FastMCP: {info.fastmcp_version}")
+ console.print(f" MCP: {info.mcp_version}")
+ console.print()
+
+ console.print(
+ "[dim]Use --format \\[fastmcp|mcp] for complete JSON output[/dim]"
+ )
+ return
+
+ # Generate formatted JSON output
+ formatted_json = await format_info(server, format, info)
+
+ # Output to file or stdout
+ if output:
+ # Ensure output directory exists
+ output.parent.mkdir(parents=True, exist_ok=True)
+
+ # Write JSON report
+ with output.open("wb") as f:
+ f.write(formatted_json)
+
+ logger.info(f"Server inspection complete. Report saved to {output}")
+
+ # Print confirmation to console
+ console.print(
+ f"[bold green]✓[/bold green] Server inspection saved to: [cyan]{output}[/cyan]"
+ )
+ console.print(f" Server: [bold]{info.name}[/bold]")
+ console.print(f" Format: {format.value}")
+ else:
+ # Output JSON to stdout
+ console.print(formatted_json.decode("utf-8"))
except Exception as e:
logger.error(
diff --git a/src/fastmcp/cli/install/claude_code.py b/src/fastmcp/cli/install/claude_code.py
index 472a34c54..aaa07503c 100644
--- a/src/fastmcp/cli/install/claude_code.py
+++ b/src/fastmcp/cli/install/claude_code.py
@@ -75,7 +75,7 @@ def install_claude_code(
server_object: str | None,
name: str,
*,
- with_editable: Path | None = None,
+ with_editable: list[Path] | None = None,
with_packages: list[str] | None = None,
env_vars: dict[str, str] | None = None,
python_version: str | None = None,
@@ -88,7 +88,7 @@ def install_claude_code(
file: Path to the server file
server_object: Optional server object name (for :object suffix)
name: Name for the server in Claude Code
- with_editable: Optional directory to install in editable mode
+ with_editable: Optional list of directories to install in editable mode
with_packages: Optional list of additional packages to install
env_vars: Optional dictionary of environment variables
python_version: Optional Python version to use
@@ -121,7 +121,7 @@ def install_claude_code(
dependencies=deduplicated_packages,
requirements=str(with_requirements) if with_requirements else None,
project=str(project) if project else None,
- editable=[str(with_editable)] if with_editable else None,
+ editable=[str(p) for p in with_editable] if with_editable else None,
)
args = env_config.build_uv_args()
@@ -171,28 +171,29 @@ async def claude_code_command(
),
] = None,
with_editable: Annotated[
- Path | None,
+ list[Path] | None,
cyclopts.Parameter(
- name=["--with-editable", "-e"],
- help="Directory with pyproject.toml to install in editable mode",
+ "--with-editable",
+ help="Directory with pyproject.toml to install in editable mode (can be used multiple times)",
+ negative="",
),
] = None,
with_packages: Annotated[
- list[str],
+ list[str] | None,
cyclopts.Parameter(
"--with",
- help="Additional packages to install",
+ help="Additional packages to install (can be used multiple times)",
negative="",
),
- ] = [],
+ ] = None,
env_vars: Annotated[
- list[str],
+ list[str] | None,
cyclopts.Parameter(
"--env",
- help="Environment variables in KEY=VALUE format",
+ help="Environment variables in KEY=VALUE format (can be used multiple times)",
negative="",
),
- ] = [],
+ ] = None,
env_file: Annotated[
Path | None,
cyclopts.Parameter(
@@ -227,6 +228,10 @@ async def claude_code_command(
Args:
server_spec: Python file to install, optionally with :object suffix
"""
+ # Convert None to empty lists for list parameters
+ with_editable = with_editable or []
+ with_packages = with_packages or []
+ env_vars = env_vars or []
file, server_object, name, packages, env_dict = await process_common_args(
server_spec, server_name, with_packages, env_vars, env_file
)
diff --git a/src/fastmcp/cli/install/claude_desktop.py b/src/fastmcp/cli/install/claude_desktop.py
index 69e543415..28ce20eee 100644
--- a/src/fastmcp/cli/install/claude_desktop.py
+++ b/src/fastmcp/cli/install/claude_desktop.py
@@ -40,7 +40,7 @@ def install_claude_desktop(
server_object: str | None,
name: str,
*,
- with_editable: Path | None = None,
+ with_editable: list[Path] | None = None,
with_packages: list[str] | None = None,
env_vars: dict[str, str] | None = None,
python_version: str | None = None,
@@ -53,7 +53,7 @@ def install_claude_desktop(
file: Path to the server file
server_object: Optional server object name (for :object suffix)
name: Name for the server in Claude's config
- with_editable: Optional directory to install in editable mode
+ with_editable: Optional list of directories to install in editable mode
with_packages: Optional list of additional packages to install
env_vars: Optional dictionary of environment variables
python_version: Optional Python version to use
@@ -86,7 +86,7 @@ def install_claude_desktop(
dependencies=deduplicated_packages,
requirements=str(with_requirements) if with_requirements else None,
project=str(project) if project else None,
- editable=[str(with_editable)] if with_editable else None,
+ editable=[str(p) for p in with_editable] if with_editable else None,
)
args = env_config.build_uv_args()
@@ -143,28 +143,29 @@ async def claude_desktop_command(
),
] = None,
with_editable: Annotated[
- Path | None,
+ list[Path] | None,
cyclopts.Parameter(
- name=["--with-editable", "-e"],
- help="Directory with pyproject.toml to install in editable mode",
+ "--with-editable",
+ help="Directory with pyproject.toml to install in editable mode (can be used multiple times)",
+ negative="",
),
] = None,
with_packages: Annotated[
- list[str],
+ list[str] | None,
cyclopts.Parameter(
"--with",
- help="Additional packages to install",
+ help="Additional packages to install (can be used multiple times)",
negative="",
),
- ] = [],
+ ] = None,
env_vars: Annotated[
- list[str],
+ list[str] | None,
cyclopts.Parameter(
"--env",
- help="Environment variables in KEY=VALUE format",
+ help="Environment variables in KEY=VALUE format (can be used multiple times)",
negative="",
),
- ] = [],
+ ] = None,
env_file: Annotated[
Path | None,
cyclopts.Parameter(
@@ -199,6 +200,10 @@ async def claude_desktop_command(
Args:
server_spec: Python file to install, optionally with :object suffix
"""
+ # Convert None to empty lists for list parameters
+ with_editable = with_editable or []
+ with_packages = with_packages or []
+ env_vars = env_vars or []
file, server_object, name, with_packages, env_dict = await process_common_args(
server_spec, server_name, with_packages, env_vars, env_file
)
diff --git a/src/fastmcp/cli/install/cursor.py b/src/fastmcp/cli/install/cursor.py
index 472dd610f..bbb94a872 100644
--- a/src/fastmcp/cli/install/cursor.py
+++ b/src/fastmcp/cli/install/cursor.py
@@ -71,7 +71,7 @@ def install_cursor_workspace(
name: str,
workspace_path: Path,
*,
- with_editable: Path | None = None,
+ with_editable: list[Path] | None = None,
with_packages: list[str] | None = None,
env_vars: dict[str, str] | None = None,
python_version: str | None = None,
@@ -85,7 +85,7 @@ def install_cursor_workspace(
server_object: Optional server object name (for :object suffix)
name: Name for the server in Cursor
workspace_path: Path to the workspace directory
- with_editable: Optional directory to install in editable mode
+ with_editable: Optional list of directories to install in editable mode
with_packages: Optional list of additional packages to install
env_vars: Optional dictionary of environment variables
python_version: Optional Python version to use
@@ -120,7 +120,7 @@ def install_cursor_workspace(
dependencies=deduplicated_packages,
requirements=str(with_requirements.resolve()) if with_requirements else None,
project=str(project.resolve()) if project else None,
- editable=[str(with_editable.resolve())] if with_editable else None,
+ editable=[str(p.resolve()) for p in with_editable] if with_editable else None,
)
args = env_config.build_uv_args()
@@ -161,7 +161,7 @@ def install_cursor(
server_object: str | None,
name: str,
*,
- with_editable: Path | None = None,
+ with_editable: list[Path] | None = None,
with_packages: list[str] | None = None,
env_vars: dict[str, str] | None = None,
python_version: str | None = None,
@@ -175,7 +175,7 @@ def install_cursor(
file: Path to the server file
server_object: Optional server object name (for :object suffix)
name: Name for the server in Cursor
- with_editable: Optional directory to install in editable mode
+ with_editable: Optional list of directories to install in editable mode
with_packages: Optional list of additional packages to install
env_vars: Optional dictionary of environment variables
python_version: Optional Python version to use
@@ -200,7 +200,7 @@ def install_cursor(
dependencies=deduplicated_packages,
requirements=str(with_requirements.resolve()) if with_requirements else None,
project=str(project.resolve()) if project else None,
- editable=[str(with_editable.resolve())] if with_editable else None,
+ editable=[str(p.resolve()) for p in with_editable] if with_editable else None,
)
args = env_config.build_uv_args()
@@ -262,28 +262,29 @@ async def cursor_command(
),
] = None,
with_editable: Annotated[
- Path | None,
+ list[Path] | None,
cyclopts.Parameter(
- name=["--with-editable", "-e"],
- help="Directory with pyproject.toml to install in editable mode",
+ "--with-editable",
+ help="Directory with pyproject.toml to install in editable mode (can be used multiple times)",
+ negative="",
),
] = None,
with_packages: Annotated[
- list[str],
+ list[str] | None,
cyclopts.Parameter(
"--with",
- help="Additional packages to install",
+ help="Additional packages to install (can be used multiple times)",
negative="",
),
- ] = [],
+ ] = None,
env_vars: Annotated[
- list[str],
+ list[str] | None,
cyclopts.Parameter(
"--env",
- help="Environment variables in KEY=VALUE format",
+ help="Environment variables in KEY=VALUE format (can be used multiple times)",
negative="",
),
- ] = [],
+ ] = None,
env_file: Annotated[
Path | None,
cyclopts.Parameter(
@@ -325,6 +326,10 @@ async def cursor_command(
Args:
server_spec: Python file to install, optionally with :object suffix
"""
+ # Convert None to empty lists for list parameters
+ with_editable = with_editable or []
+ with_packages = with_packages or []
+ env_vars = env_vars or []
file, server_object, name, with_packages, env_dict = await process_common_args(
server_spec, server_name, with_packages, env_vars, env_file
)
diff --git a/src/fastmcp/cli/install/mcp_json.py b/src/fastmcp/cli/install/mcp_json.py
index 9315c40ef..42696ae6c 100644
--- a/src/fastmcp/cli/install/mcp_json.py
+++ b/src/fastmcp/cli/install/mcp_json.py
@@ -22,7 +22,7 @@ def install_mcp_json(
server_object: str | None,
name: str,
*,
- with_editable: Path | None = None,
+ with_editable: list[Path] | None = None,
with_packages: list[str] | None = None,
env_vars: dict[str, str] | None = None,
copy: bool = False,
@@ -36,7 +36,7 @@ def install_mcp_json(
file: Path to the server file
server_object: Optional server object name (for :object suffix)
name: Name for the server in MCP config
- with_editable: Optional directory to install in editable mode
+ with_editable: Optional list of directories to install in editable mode
with_packages: Optional list of additional packages to install
env_vars: Optional dictionary of environment variables
copy: If True, copy to clipboard instead of printing to stdout
@@ -61,7 +61,7 @@ def install_mcp_json(
dependencies=deduplicated_packages,
requirements=str(with_requirements) if with_requirements else None,
project=str(project) if project else None,
- editable=[str(with_editable)] if with_editable else None,
+ editable=[str(p) for p in with_editable] if with_editable else None,
)
args = env_config.build_uv_args()
@@ -116,28 +116,29 @@ async def mcp_json_command(
),
] = None,
with_editable: Annotated[
- Path | None,
+ list[Path] | None,
cyclopts.Parameter(
- name=["--with-editable", "-e"],
- help="Directory with pyproject.toml to install in editable mode",
+ "--with-editable",
+ help="Directory with pyproject.toml to install in editable mode (can be used multiple times)",
+ negative="",
),
] = None,
with_packages: Annotated[
- list[str],
+ list[str] | None,
cyclopts.Parameter(
"--with",
- help="Additional packages to install",
+ help="Additional packages to install (can be used multiple times)",
negative="",
),
- ] = [],
+ ] = None,
env_vars: Annotated[
- list[str],
+ list[str] | None,
cyclopts.Parameter(
"--env",
- help="Environment variables in KEY=VALUE format",
+ help="Environment variables in KEY=VALUE format (can be used multiple times)",
negative="",
),
- ] = [],
+ ] = None,
env_file: Annotated[
Path | None,
cyclopts.Parameter(
@@ -180,6 +181,10 @@ async def mcp_json_command(
Args:
server_spec: Python file to install, optionally with :object suffix
"""
+ # Convert None to empty lists for list parameters
+ with_editable = with_editable or []
+ with_packages = with_packages or []
+ env_vars = env_vars or []
file, server_object, name, packages, env_dict = await process_common_args(
server_spec, server_name, with_packages, env_vars, env_file
)
diff --git a/src/fastmcp/cli/install/shared.py b/src/fastmcp/cli/install/shared.py
index 2f92fbd89..6db83403f 100644
--- a/src/fastmcp/cli/install/shared.py
+++ b/src/fastmcp/cli/install/shared.py
@@ -29,14 +29,17 @@ def parse_env_var(env_var: str) -> tuple[str, str]:
async def process_common_args(
server_spec: str,
server_name: str | None,
- with_packages: list[str],
- env_vars: list[str],
+ with_packages: list[str] | None,
+ env_vars: list[str] | None,
env_file: Path | None,
) -> tuple[Path, str | None, str, list[str], dict[str, str] | None]:
"""Process common arguments shared by all install commands.
Handles both fastmcp.json config files and traditional file.py:object syntax.
"""
+ # Convert None to empty lists for list parameters
+ with_packages = with_packages or []
+ env_vars = env_vars or []
# Create FastMCPConfig from server_spec
config = None
if server_spec.endswith(".json"):
diff --git a/src/fastmcp/server/auth/providers/azure.py b/src/fastmcp/server/auth/providers/azure.py
index 2bb0b924c..a4de21021 100644
--- a/src/fastmcp/server/auth/providers/azure.py
+++ b/src/fastmcp/server/auth/providers/azure.py
@@ -7,12 +7,13 @@ using the OAuth Proxy pattern for non-DCR OAuth flows.
from __future__ import annotations
import httpx
-from pydantic import SecretStr
+from pydantic import SecretStr, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from fastmcp.server.auth import AccessToken, TokenVerifier
from fastmcp.server.auth.oauth_proxy import OAuthProxy
from fastmcp.server.auth.registry import register_provider
+from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import NotSet, NotSetT
@@ -36,6 +37,11 @@ class AzureProviderSettings(BaseSettings):
required_scopes: list[str] | None = None
timeout_seconds: int | None = None
+ @field_validator("required_scopes", mode="before")
+ @classmethod
+ def _parse_scopes(cls, v):
+ return parse_scopes(v)
+
class AzureTokenVerifier(TokenVerifier):
"""Token verifier for Azure OAuth tokens.
diff --git a/src/fastmcp/server/auth/providers/github.py b/src/fastmcp/server/auth/providers/github.py
index cddf83fdf..4f3fcc862 100644
--- a/src/fastmcp/server/auth/providers/github.py
+++ b/src/fastmcp/server/auth/providers/github.py
@@ -22,13 +22,14 @@ Example:
from __future__ import annotations
import httpx
-from pydantic import AnyHttpUrl, SecretStr
+from pydantic import AnyHttpUrl, SecretStr, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from fastmcp.server.auth import TokenVerifier
from fastmcp.server.auth.auth import AccessToken
from fastmcp.server.auth.oauth_proxy import OAuthProxy
from fastmcp.server.auth.registry import register_provider
+from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import NotSet, NotSetT
@@ -51,6 +52,11 @@ class GitHubProviderSettings(BaseSettings):
required_scopes: list[str] | None = None
timeout_seconds: int | None = None
+ @field_validator("required_scopes", mode="before")
+ @classmethod
+ def _parse_scopes(cls, v):
+ return parse_scopes(v)
+
class GitHubTokenVerifier(TokenVerifier):
"""Token verifier for GitHub OAuth tokens.
diff --git a/src/fastmcp/server/auth/providers/google.py b/src/fastmcp/server/auth/providers/google.py
index 48f61bf8a..920034395 100644
--- a/src/fastmcp/server/auth/providers/google.py
+++ b/src/fastmcp/server/auth/providers/google.py
@@ -24,13 +24,14 @@ from __future__ import annotations
import time
import httpx
-from pydantic import AnyHttpUrl, SecretStr
+from pydantic import AnyHttpUrl, SecretStr, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from fastmcp.server.auth import TokenVerifier
from fastmcp.server.auth.auth import AccessToken
from fastmcp.server.auth.oauth_proxy import OAuthProxy
from fastmcp.server.auth.registry import register_provider
+from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import NotSet, NotSetT
@@ -53,6 +54,11 @@ class GoogleProviderSettings(BaseSettings):
required_scopes: list[str] | None = None
timeout_seconds: int | None = None
+ @field_validator("required_scopes", mode="before")
+ @classmethod
+ def _parse_scopes(cls, v):
+ return parse_scopes(v)
+
class GoogleTokenVerifier(TokenVerifier):
"""Token verifier for Google OAuth tokens.
diff --git a/src/fastmcp/server/auth/providers/jwt.py b/src/fastmcp/server/auth/providers/jwt.py
index dee1bb478..19eac1bb2 100644
--- a/src/fastmcp/server/auth/providers/jwt.py
+++ b/src/fastmcp/server/auth/providers/jwt.py
@@ -11,12 +11,13 @@ from authlib.jose import JsonWebKey, JsonWebToken
from authlib.jose.errors import JoseError
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
-from pydantic import AnyHttpUrl, SecretStr
+from pydantic import AnyHttpUrl, SecretStr, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from typing_extensions import TypedDict
from fastmcp.server.auth import AccessToken, TokenVerifier
from fastmcp.server.auth.registry import register_provider
+from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import NotSet, NotSetT
@@ -155,6 +156,11 @@ class JWTVerifierSettings(BaseSettings):
required_scopes: list[str] | None = None
resource_server_url: AnyHttpUrl | str | None = None
+ @field_validator("required_scopes", mode="before")
+ @classmethod
+ def _parse_scopes(cls, v):
+ return parse_scopes(v)
+
@register_provider("JWT")
class JWTVerifier(TokenVerifier):
diff --git a/src/fastmcp/server/auth/providers/workos.py b/src/fastmcp/server/auth/providers/workos.py
index a1d77fd08..2fc138141 100644
--- a/src/fastmcp/server/auth/providers/workos.py
+++ b/src/fastmcp/server/auth/providers/workos.py
@@ -11,7 +11,7 @@ Choose based on your WorkOS setup and authentication requirements.
from __future__ import annotations
import httpx
-from pydantic import AnyHttpUrl, SecretStr
+from pydantic import AnyHttpUrl, SecretStr, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from starlette.responses import JSONResponse
from starlette.routing import Route
@@ -20,6 +20,7 @@ from fastmcp.server.auth import AccessToken, RemoteAuthProvider, TokenVerifier
from fastmcp.server.auth.oauth_proxy import OAuthProxy
from fastmcp.server.auth.providers.jwt import JWTVerifier
from fastmcp.server.auth.registry import register_provider
+from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import NotSet, NotSetT
@@ -43,6 +44,11 @@ class WorkOSProviderSettings(BaseSettings):
required_scopes: list[str] | None = None
timeout_seconds: int | None = None
+ @field_validator("required_scopes", mode="before")
+ @classmethod
+ def _parse_scopes(cls, v):
+ return parse_scopes(v)
+
class WorkOSTokenVerifier(TokenVerifier):
"""Token verifier for WorkOS OAuth tokens.
@@ -255,6 +261,11 @@ class AuthKitProviderSettings(BaseSettings):
base_url: AnyHttpUrl
required_scopes: list[str] | None = None
+ @field_validator("required_scopes", mode="before")
+ @classmethod
+ def _parse_scopes(cls, v):
+ return parse_scopes(v)
+
@register_provider("AUTHKIT")
class AuthKitProvider(RemoteAuthProvider):
diff --git a/src/fastmcp/utilities/auth.py b/src/fastmcp/utilities/auth.py
new file mode 100644
index 000000000..bec370ac7
--- /dev/null
+++ b/src/fastmcp/utilities/auth.py
@@ -0,0 +1,34 @@
+"""Authentication utility helpers."""
+
+from __future__ import annotations
+
+import json
+from typing import Any
+
+
+def parse_scopes(value: Any) -> list[str] | None:
+ """Parse scopes from environment variables or settings values.
+
+ Accepts either a JSON array string, a comma- or space-separated string,
+ a list of strings, or ``None``. Returns a list of scopes or ``None`` if
+ no value is provided.
+ """
+ if value is None or value == "":
+ return None if value is None else []
+ if isinstance(value, list):
+ return [str(v).strip() for v in value if str(v).strip()]
+ if isinstance(value, str):
+ value = value.strip()
+ if not value:
+ return []
+ # Try JSON array first
+ if value.startswith("["):
+ try:
+ data = json.loads(value)
+ if isinstance(data, list):
+ return [str(v).strip() for v in data if str(v).strip()]
+ except Exception:
+ pass
+ # Fallback to comma/space separated list
+ return [s.strip() for s in value.replace(",", " ").split() if s.strip()]
+ return value
diff --git a/src/fastmcp/utilities/inspect.py b/src/fastmcp/utilities/inspect.py
index df38d679e..f3303a8f4 100644
--- a/src/fastmcp/utilities/inspect.py
+++ b/src/fastmcp/utilities/inspect.py
@@ -4,8 +4,10 @@ from __future__ import annotations
import importlib.metadata
from dataclasses import dataclass
-from typing import Any, cast
+from enum import Enum
+from typing import Any, Literal, cast
+import pydantic_core
from mcp.server.fastmcp import FastMCP as FastMCP1x
import fastmcp
@@ -21,9 +23,12 @@ class ToolInfo:
name: str
description: str | None
input_schema: dict[str, Any]
+ output_schema: dict[str, Any] | None = None
annotations: dict[str, Any] | None = None
tags: list[str] | None = None
enabled: bool | None = None
+ title: str | None = None
+ meta: dict[str, Any] | None = None
@dataclass
@@ -36,6 +41,8 @@ class PromptInfo:
arguments: list[dict[str, Any]] | None = None
tags: list[str] | None = None
enabled: bool | None = None
+ title: str | None = None
+ meta: dict[str, Any] | None = None
@dataclass
@@ -47,8 +54,11 @@ class ResourceInfo:
name: str | None
description: str | None
mime_type: str | None = None
+ annotations: dict[str, Any] | None = None
tags: list[str] | None = None
enabled: bool | None = None
+ title: str | None = None
+ meta: dict[str, Any] | None = None
@dataclass
@@ -60,8 +70,12 @@ class TemplateInfo:
name: str | None
description: str | None
mime_type: str | None = None
+ parameters: dict[str, Any] | None = None
+ annotations: dict[str, Any] | None = None
tags: list[str] | None = None
enabled: bool | None = None
+ title: str | None = None
+ meta: dict[str, Any] | None = None
@dataclass
@@ -70,9 +84,10 @@ class FastMCPInfo:
name: str
instructions: str | None
- fastmcp_version: str
- mcp_version: str
- server_version: str | None
+ version: str | None # The server's own version string (if specified)
+ fastmcp_version: str # Version of FastMCP generating this manifest
+ mcp_version: str # Version of MCP protocol library
+ server_generation: int # Server generation: 1 (mcp package) or 2 (fastmcp)
tools: list[ToolInfo]
prompts: list[PromptInfo]
resources: list[ResourceInfo]
@@ -106,9 +121,12 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
name=tool.name or key,
description=tool.description,
input_schema=mcp_tool.inputSchema if mcp_tool.inputSchema else {},
+ output_schema=tool.output_schema,
annotations=tool.annotations.model_dump() if tool.annotations else None,
tags=list(tool.tags) if tool.tags else None,
enabled=tool.enabled,
+ title=tool.title,
+ meta=tool.meta,
)
)
@@ -125,6 +143,8 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
else None,
tags=list(prompt.tags) if prompt.tags else None,
enabled=prompt.enabled,
+ title=prompt.title,
+ meta=prompt.meta,
)
)
@@ -138,8 +158,13 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
name=resource.name,
description=resource.description,
mime_type=resource.mime_type,
+ annotations=resource.annotations.model_dump()
+ if resource.annotations
+ else None,
tags=list(resource.tags) if resource.tags else None,
enabled=resource.enabled,
+ title=resource.title,
+ meta=resource.meta,
)
)
@@ -153,8 +178,14 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
name=template.name,
description=template.description,
mime_type=template.mime_type,
+ parameters=template.parameters,
+ annotations=template.annotations.model_dump()
+ if template.annotations
+ else None,
tags=list(template.tags) if template.tags else None,
enabled=template.enabled,
+ title=template.title,
+ meta=template.meta,
)
)
@@ -171,9 +202,8 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
instructions=mcp.instructions,
fastmcp_version=fastmcp.__version__,
mcp_version=importlib.metadata.version("mcp"),
- server_version=(
- mcp.version if hasattr(mcp, "version") else mcp._mcp_server.version
- ),
+ server_generation=2, # FastMCP v2
+ version=(mcp.version if hasattr(mcp, "version") else mcp._mcp_server.version),
tools=tool_infos,
prompts=prompt_infos,
resources=resource_infos,
@@ -191,7 +221,6 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
Returns:
FastMCPInfo dataclass containing the extracted information
"""
-
# Use a client to interact with the FastMCP1x server
async with Client(mcp) as client:
# Get components via client calls (these return MCP objects)
@@ -208,25 +237,18 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
# Extract detailed tool information from MCP Tool objects
tool_infos = []
for mcp_tool in mcp_tools:
- # Extract annotations if they exist
- annotations = None
- if hasattr(mcp_tool, "annotations") and mcp_tool.annotations:
- if hasattr(mcp_tool.annotations, "model_dump"):
- annotations = mcp_tool.annotations.model_dump()
- elif isinstance(mcp_tool.annotations, dict):
- annotations = mcp_tool.annotations
- else:
- annotations = None
-
tool_infos.append(
ToolInfo(
- key=mcp_tool.name, # For 1.x, key and name are the same
+ key=mcp_tool.name,
name=mcp_tool.name,
description=mcp_tool.description,
input_schema=mcp_tool.inputSchema if mcp_tool.inputSchema else {},
- annotations=annotations,
- tags=None, # 1.x doesn't have tags
- enabled=None, # 1.x doesn't have enabled field
+ output_schema=None, # v1 doesn't have output_schema
+ annotations=None, # v1 doesn't have annotations
+ tags=None, # v1 doesn't have tags
+ enabled=None, # v1 doesn't have enabled field
+ title=None, # v1 doesn't have title
+ meta=None, # v1 doesn't have meta field
)
)
@@ -240,12 +262,14 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
prompt_infos.append(
PromptInfo(
- key=mcp_prompt.name, # For 1.x, key and name are the same
+ key=mcp_prompt.name,
name=mcp_prompt.name,
description=mcp_prompt.description,
arguments=arguments,
- tags=None, # 1.x doesn't have tags
- enabled=None, # 1.x doesn't have enabled field
+ tags=None, # v1 doesn't have tags
+ enabled=None, # v1 doesn't have enabled field
+ title=None, # v1 doesn't have title
+ meta=None, # v1 doesn't have meta field
)
)
@@ -254,13 +278,16 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
for mcp_resource in mcp_resources:
resource_infos.append(
ResourceInfo(
- key=str(mcp_resource.uri), # For 1.x, key and uri are the same
+ key=str(mcp_resource.uri),
uri=str(mcp_resource.uri),
name=mcp_resource.name,
description=mcp_resource.description,
mime_type=mcp_resource.mimeType,
- tags=None, # 1.x doesn't have tags
- enabled=None, # 1.x doesn't have enabled field
+ annotations=None, # v1 doesn't have annotations
+ tags=None, # v1 doesn't have tags
+ enabled=None, # v1 doesn't have enabled field
+ title=None, # v1 doesn't have title
+ meta=None, # v1 doesn't have meta field
)
)
@@ -269,15 +296,17 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
for mcp_template in mcp_templates:
template_infos.append(
TemplateInfo(
- key=str(
- mcp_template.uriTemplate
- ), # For 1.x, key and uriTemplate are the same
+ key=str(mcp_template.uriTemplate),
uri_template=str(mcp_template.uriTemplate),
name=mcp_template.name,
description=mcp_template.description,
mime_type=mcp_template.mimeType,
- tags=None, # 1.x doesn't have tags
- enabled=None, # 1.x doesn't have enabled field
+ parameters=None, # v1 doesn't expose template parameters
+ annotations=None, # v1 doesn't have annotations
+ tags=None, # v1 doesn't have tags
+ enabled=None, # v1 doesn't have enabled field
+ title=None, # v1 doesn't have title
+ meta=None, # v1 doesn't have meta field
)
)
@@ -292,13 +321,14 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
return FastMCPInfo(
name=mcp._mcp_server.name,
instructions=mcp._mcp_server.instructions,
- fastmcp_version=importlib.metadata.version("mcp"),
+ fastmcp_version=fastmcp.__version__, # Version generating this manifest
mcp_version=importlib.metadata.version("mcp"),
- server_version=mcp._mcp_server.version,
+ server_generation=1, # MCP v1
+ version=mcp._mcp_server.version,
tools=tool_infos,
prompts=prompt_infos,
resources=resource_infos,
- templates=template_infos, # FastMCP1x does have templates
+ templates=template_infos,
capabilities=capabilities,
)
@@ -319,3 +349,102 @@ async def inspect_fastmcp(mcp: FastMCP[Any] | FastMCP1x) -> FastMCPInfo:
return await inspect_fastmcp_v1(mcp)
else:
return await inspect_fastmcp_v2(cast(FastMCP[Any], mcp))
+
+
+class InspectFormat(str, Enum):
+ """Output format for inspect command."""
+
+ FASTMCP = "fastmcp"
+ MCP = "mcp"
+
+
+async def format_fastmcp_info(info: FastMCPInfo) -> bytes:
+ """Format FastMCPInfo as FastMCP-specific JSON.
+
+ This includes FastMCP-specific fields like tags, enabled, annotations, etc.
+ """
+ # Build the output dict with nested structure
+ result = {
+ "server": {
+ "name": info.name,
+ "instructions": info.instructions,
+ "version": info.version,
+ "generation": info.server_generation,
+ "capabilities": info.capabilities,
+ },
+ "environment": {
+ "fastmcp": info.fastmcp_version,
+ "mcp": info.mcp_version,
+ },
+ "tools": info.tools,
+ "prompts": info.prompts,
+ "resources": info.resources,
+ "templates": info.templates,
+ }
+
+ return pydantic_core.to_json(result, indent=2)
+
+
+async def format_mcp_info(mcp: FastMCP[Any] | FastMCP1x) -> bytes:
+ """Format server info as standard MCP protocol JSON.
+
+ Uses Client to get the standard MCP protocol format with camelCase fields.
+ Includes version metadata at the top level.
+ """
+ async with Client(mcp) as client:
+ # Get all the MCP protocol objects
+ tools_result = await client.list_tools_mcp()
+ prompts_result = await client.list_prompts_mcp()
+ resources_result = await client.list_resources_mcp()
+ templates_result = await client.list_resource_templates_mcp()
+
+ # Get server info from the initialize result
+ server_info = client.initialize_result.serverInfo
+
+ # Combine into MCP protocol structure with environment metadata
+ result = {
+ "environment": {
+ "fastmcp": fastmcp.__version__, # Version generating this manifest
+ "mcp": importlib.metadata.version("mcp"), # MCP protocol version
+ },
+ "serverInfo": server_info,
+ "capabilities": {}, # MCP format doesn't include capabilities at top level
+ "tools": tools_result.tools,
+ "prompts": prompts_result.prompts,
+ "resources": resources_result.resources,
+ "resourceTemplates": templates_result.resourceTemplates,
+ }
+
+ return pydantic_core.to_json(result, indent=2)
+
+
+async def format_info(
+ mcp: FastMCP[Any] | FastMCP1x,
+ format: InspectFormat | Literal["fastmcp", "mcp"],
+ info: FastMCPInfo | None = None,
+) -> bytes:
+ """Format server information according to the specified format.
+
+ Args:
+ mcp: The FastMCP instance
+ format: Output format ("fastmcp" or "mcp")
+ info: Pre-extracted FastMCPInfo (optional, will be extracted if not provided)
+
+ Returns:
+ JSON bytes in the requested format
+ """
+ # Convert string to enum if needed
+ if isinstance(format, str):
+ format = InspectFormat(format)
+
+ if format == InspectFormat.MCP:
+ # MCP format doesn't need FastMCPInfo, it uses Client directly
+ return await format_mcp_info(mcp)
+ elif format == InspectFormat.FASTMCP:
+ # For FastMCP format, we need the FastMCPInfo
+ # This works for both v1 and v2 servers
+ if info is None:
+ info = await inspect_fastmcp(mcp)
+ return await format_fastmcp_info(info)
+ else:
+ raise ValueError(f"Unknown format: {format}")
diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py
index d4995a515..1d0d078a6 100644
--- a/tests/cli/test_cli.py
+++ b/tests/cli/test_cli.py
@@ -509,6 +509,42 @@ class TestInspectCommand:
# Output is parsed as a Path object
assert bound.arguments["output"] == output_file
+ async def test_inspect_command_text_summary(self, tmp_path, capsys):
+ """Test inspect command with no format shows text summary."""
+ # Create a real server file
+ server_file = tmp_path / "test_server.py"
+ server_file.write_text("""
+import fastmcp
+
+mcp = fastmcp.FastMCP("InspectTestServer", instructions="Test instructions", version="1.0.0")
+
+@mcp.tool
+def test_tool(x: int) -> int:
+ return x * 2
+""")
+
+ # Parse and execute the command without format or output
+ command, bound, _ = app.parse_args(
+ [
+ "inspect",
+ str(server_file),
+ ]
+ )
+
+ await command(**bound.arguments)
+
+ # Check the console output
+ captured = capsys.readouterr()
+ # Check for the table format output
+ assert "InspectTestServer" in captured.out
+ assert "Test instructions" in captured.out
+ assert "1.0.0" in captured.out
+ assert "Tools" in captured.out
+ assert "1" in captured.out # number of tools
+ assert "FastMCP" in captured.out
+ assert "MCP" in captured.out
+ assert "Use --format [fastmcp|mcp] for complete JSON output" in captured.out
+
async def test_inspect_command_with_real_server(self, tmp_path):
"""Test inspect command with a real server file."""
# Create a real server file
@@ -529,11 +565,13 @@ def test_prompt(name: str) -> str:
output_file = tmp_path / "inspect_output.json"
- # Parse and execute the command
+ # Parse and execute the command with format and output file
command, bound, _ = app.parse_args(
[
"inspect",
str(server_file),
+ "--format",
+ "fastmcp",
"--output",
str(output_file),
]
@@ -545,7 +583,10 @@ def test_prompt(name: str) -> str:
assert output_file.exists()
content = output_file.read_text()
- # Basic checks that the inspection worked
- assert "InspectTestServer" in content
- assert "test_tool" in content
- assert "test_prompt" in content
+ # Basic checks that the fastmcp format worked
+ import json
+
+ data = json.loads(content)
+ assert data["server"]["name"] == "InspectTestServer"
+ assert len(data["tools"]) == 1
+ assert len(data["prompts"]) == 1
diff --git a/tests/cli/test_cursor.py b/tests/cli/test_cursor.py
index 008aff843..c5531bd8d 100644
--- a/tests/cli/test_cursor.py
+++ b/tests/cli/test_cursor.py
@@ -246,7 +246,7 @@ class TestInstallCursor:
file=Path("/path/to/server.py"),
server_object="custom_app",
name="test-server",
- with_editable=editable_path,
+ with_editable=[editable_path],
)
assert result is True
@@ -328,7 +328,7 @@ class TestCursorCommand:
file=Path("server.py"),
server_object=None,
name="test-server",
- with_editable=None,
+ with_editable=[],
with_packages=[],
env_vars={},
python_version=None,
diff --git a/tests/server/auth/providers/test_azure.py b/tests/server/auth/providers/test_azure.py
index b47c0e837..7f76502be 100644
--- a/tests/server/auth/providers/test_azure.py
+++ b/tests/server/auth/providers/test_azure.py
@@ -31,7 +31,14 @@ class TestAzureProvider:
parsed_token = urlparse(provider._upstream_token_endpoint)
assert "87654321-4321-4321-4321-210987654321" in parsed_token.path
- def test_init_with_env_vars(self):
+ @pytest.mark.parametrize(
+ "scopes_env",
+ [
+ "User.Read,Calendar.Read",
+ '["User.Read", "Calendar.Read"]',
+ ],
+ )
+ def test_init_with_env_vars(self, scopes_env):
"""Test AzureProvider initialization from environment variables."""
with patch.dict(
os.environ,
@@ -40,7 +47,7 @@ class TestAzureProvider:
"FASTMCP_SERVER_AUTH_AZURE_CLIENT_SECRET": "env-secret",
"FASTMCP_SERVER_AUTH_AZURE_TENANT_ID": "env-tenant-id",
"FASTMCP_SERVER_AUTH_AZURE_BASE_URL": "https://envserver.com",
- "FASTMCP_SERVER_AUTH_AZURE_REQUIRED_SCOPES": '["User.Read", "Calendar.Read"]',
+ "FASTMCP_SERVER_AUTH_AZURE_REQUIRED_SCOPES": scopes_env,
},
):
provider = AzureProvider()
@@ -48,6 +55,10 @@ class TestAzureProvider:
assert provider._upstream_client_id == "env-client-id"
assert provider._upstream_client_secret.get_secret_value() == "env-secret"
assert str(provider.base_url) == "https://envserver.com/"
+ assert provider._token_validator.required_scopes == [
+ "User.Read",
+ "Calendar.Read",
+ ]
# Check tenant is in the endpoints
parsed_auth = urlparse(provider._upstream_authorization_endpoint)
assert "env-tenant-id" in parsed_auth.path
diff --git a/tests/server/auth/providers/test_github.py b/tests/server/auth/providers/test_github.py
index 7c3725f31..198105774 100644
--- a/tests/server/auth/providers/test_github.py
+++ b/tests/server/auth/providers/test_github.py
@@ -83,7 +83,14 @@ class TestGitHubProvider:
) # URLs get normalized with trailing slash
assert provider._redirect_path == "/custom/callback"
- def test_init_with_env_vars(self):
+ @pytest.mark.parametrize(
+ "scopes_env",
+ [
+ "user,repo",
+ '["user", "repo"]',
+ ],
+ )
+ def test_init_with_env_vars(self, scopes_env):
"""Test initialization with environment variables."""
with patch.dict(
os.environ,
@@ -91,6 +98,7 @@ class TestGitHubProvider:
"FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID": "env_client_id",
"FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET": "env_secret",
"FASTMCP_SERVER_AUTH_GITHUB_BASE_URL": "https://env-example.com",
+ "FASTMCP_SERVER_AUTH_GITHUB_REQUIRED_SCOPES": scopes_env,
},
):
provider = GitHubProvider()
@@ -98,6 +106,7 @@ class TestGitHubProvider:
assert provider._upstream_client_id == "env_client_id"
assert provider._upstream_client_secret.get_secret_value() == "env_secret"
assert str(provider.base_url) == "https://env-example.com/"
+ assert provider._token_validator.required_scopes == ["user", "repo"]
def test_init_explicit_overrides_env(self):
"""Test that explicit parameters override environment variables."""
diff --git a/tests/server/auth/providers/test_google.py b/tests/server/auth/providers/test_google.py
index 3f68eb152..4402e16fd 100644
--- a/tests/server/auth/providers/test_google.py
+++ b/tests/server/auth/providers/test_google.py
@@ -24,7 +24,14 @@ class TestGoogleProvider:
assert provider._upstream_client_secret.get_secret_value() == "GOCSPX-test123"
assert str(provider.base_url) == "https://myserver.com/"
- def test_init_with_env_vars(self):
+ @pytest.mark.parametrize(
+ "scopes_env",
+ [
+ "openid,https://www.googleapis.com/auth/userinfo.email",
+ '["openid", "https://www.googleapis.com/auth/userinfo.email"]',
+ ],
+ )
+ def test_init_with_env_vars(self, scopes_env):
"""Test GoogleProvider initialization from environment variables."""
with patch.dict(
os.environ,
@@ -32,7 +39,7 @@ class TestGoogleProvider:
"FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID": "env123.apps.googleusercontent.com",
"FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET": "GOCSPX-env456",
"FASTMCP_SERVER_AUTH_GOOGLE_BASE_URL": "https://envserver.com",
- "FASTMCP_SERVER_AUTH_GOOGLE_REQUIRED_SCOPES": '["openid", "https://www.googleapis.com/auth/userinfo.email"]',
+ "FASTMCP_SERVER_AUTH_GOOGLE_REQUIRED_SCOPES": scopes_env,
},
):
provider = GoogleProvider()
@@ -42,6 +49,10 @@ class TestGoogleProvider:
provider._upstream_client_secret.get_secret_value() == "GOCSPX-env456"
)
assert str(provider.base_url) == "https://envserver.com/"
+ assert provider._token_validator.required_scopes == [
+ "openid",
+ "https://www.googleapis.com/auth/userinfo.email",
+ ]
def test_init_missing_client_id_raises_error(self):
"""Test that missing client_id raises ValueError."""
diff --git a/tests/server/auth/providers/test_workos.py b/tests/server/auth/providers/test_workos.py
index 8f2643bef..38e23239d 100644
--- a/tests/server/auth/providers/test_workos.py
+++ b/tests/server/auth/providers/test_workos.py
@@ -26,7 +26,14 @@ class TestWorkOSProvider:
assert provider._upstream_client_secret.get_secret_value() == "secret_test456"
assert str(provider.base_url) == "https://myserver.com/"
- def test_init_with_env_vars(self):
+ @pytest.mark.parametrize(
+ "scopes_env",
+ [
+ "openid,email",
+ '["openid", "email"]',
+ ],
+ )
+ def test_init_with_env_vars(self, scopes_env):
"""Test WorkOSProvider initialization from environment variables."""
with patch.dict(
os.environ,
@@ -35,7 +42,7 @@ class TestWorkOSProvider:
"FASTMCP_SERVER_AUTH_WORKOS_CLIENT_SECRET": "env_secret",
"FASTMCP_SERVER_AUTH_WORKOS_AUTHKIT_DOMAIN": "https://env.authkit.app",
"FASTMCP_SERVER_AUTH_WORKOS_BASE_URL": "https://envserver.com",
- "FASTMCP_SERVER_AUTH_WORKOS_REQUIRED_SCOPES": '["openid", "email"]',
+ "FASTMCP_SERVER_AUTH_WORKOS_REQUIRED_SCOPES": scopes_env,
},
):
provider = WorkOSProvider()
@@ -43,6 +50,10 @@ class TestWorkOSProvider:
assert provider._upstream_client_id == "env_client"
assert provider._upstream_client_secret.get_secret_value() == "env_secret"
assert str(provider.base_url) == "https://envserver.com/"
+ assert provider._token_validator.required_scopes == [
+ "openid",
+ "email",
+ ]
def test_init_missing_client_id_raises_error(self):
"""Test that missing client_id raises ValueError."""
diff --git a/tests/utilities/test_inspect.py b/tests/utilities/test_inspect.py
index 0b734c199..e75439ea9 100644
--- a/tests/utilities/test_inspect.py
+++ b/tests/utilities/test_inspect.py
@@ -8,7 +8,11 @@ import fastmcp
from fastmcp import Client, FastMCP
from fastmcp.utilities.inspect import (
FastMCPInfo,
+ InspectFormat,
ToolInfo,
+ format_fastmcp_info,
+ format_info,
+ format_mcp_info,
inspect_fastmcp,
inspect_fastmcp_v1,
)
@@ -20,14 +24,22 @@ class TestFastMCPInfo:
def test_fastmcp_info_creation(self):
"""Test that FastMCPInfo can be created with all required fields."""
tool = ToolInfo(
- key="tool1", name="tool1", description="Test tool", input_schema={}
+ key="tool1",
+ name="tool1",
+ description="Test tool",
+ input_schema={},
+ output_schema={
+ "type": "object",
+ "properties": {"result": {"type": "string"}},
+ },
)
info = FastMCPInfo(
name="TestServer",
instructions="Test instructions",
fastmcp_version="1.0.0",
mcp_version="1.0.0",
- server_version="1.0.0",
+ server_generation=2,
+ version="1.0.0",
tools=[tool],
prompts=[],
resources=[],
@@ -39,7 +51,8 @@ class TestFastMCPInfo:
assert info.instructions == "Test instructions"
assert info.fastmcp_version == "1.0.0"
assert info.mcp_version == "1.0.0"
- assert info.server_version == "1.0.0"
+ assert info.server_generation == 2
+ assert info.version == "1.0.0"
assert len(info.tools) == 1
assert info.tools[0].name == "tool1"
assert info.capabilities == {"tools": {"listChanged": True}}
@@ -51,7 +64,8 @@ class TestFastMCPInfo:
instructions=None,
fastmcp_version="1.0.0",
mcp_version="1.0.0",
- server_version="1.0.0",
+ server_generation=2,
+ version="1.0.0",
tools=[],
prompts=[],
resources=[],
@@ -75,7 +89,8 @@ class TestGetFastMCPInfo:
assert info.instructions is None
assert info.fastmcp_version == fastmcp.__version__
assert info.mcp_version == importlib.metadata.version("mcp")
- assert info.server_version is None
+ assert info.server_generation == 2 # v2 server
+ assert info.version is None
assert info.tools == []
assert info.prompts == []
assert info.resources == []
@@ -95,7 +110,7 @@ class TestGetFastMCPInfo:
"""Test get_fastmcp_info with a server that has a version."""
mcp = FastMCP("VersionServer", version="1.2.3")
info = await inspect_fastmcp(mcp)
- assert info.server_version == "1.2.3"
+ assert info.version == "1.2.3"
async def test_server_with_tools(self):
"""Test get_fastmcp_info with a server that has tools."""
@@ -266,9 +281,10 @@ class TestFastMCP1xCompatibility:
assert info.name == "Test1x"
assert info.instructions is None
- assert info.fastmcp_version == importlib.metadata.version("mcp")
+ assert info.fastmcp_version == fastmcp.__version__ # CLI version
assert info.mcp_version == importlib.metadata.version("mcp")
- assert info.server_version is None
+ assert info.server_generation == 1 # v1 server
+ assert info.version is None
assert info.tools == []
assert info.prompts == []
assert info.resources == []
@@ -310,6 +326,7 @@ class TestFastMCP1xCompatibility:
resource_uris = [res.uri for res in info.resources]
assert "resource://data" in resource_uris
assert len(info.templates) == 0 # No templates added in this test
+ assert info.server_generation == 1 # v1 server
async def test_fastmcp1x_with_prompts(self):
"""Test get_fastmcp_info_v1 with a FastMCP1x server that has prompts."""
@@ -341,6 +358,7 @@ class TestFastMCP1xCompatibility:
tool_names = [tool.name for tool in info.tools]
assert "test_tool" in tool_names
assert len(info.templates) == 0 # No templates added in this test
+ assert info.server_generation == 1 # v1 server
async def test_dispatcher_with_fastmcp2x(self):
"""Test that the main get_fastmcp_info function correctly dispatches to v2."""
@@ -384,9 +402,191 @@ class TestFastMCP1xCompatibility:
assert "tool2x" in tool2x_names
# Check server versions
- assert info1x.server_version is None
- assert info2x.server_version is None
+ assert info1x.server_generation == 1 # v1
+ assert info2x.server_generation == 2 # v2
+ assert info1x.version is None
+ assert info2x.version is None
# No templates added in these tests
assert len(info1x.templates) == 0
assert len(info2x.templates) == 0
+
+
+class TestFormatFunctions:
+ """Tests for the formatting functions."""
+
+ async def test_format_fastmcp_info(self):
+ """Test formatting as FastMCP-specific JSON."""
+ mcp = FastMCP("TestServer", instructions="Test instructions", version="1.2.3")
+
+ @mcp.tool
+ def test_tool(x: int) -> dict:
+ """A test tool."""
+ return {"result": x * 2}
+
+ info = await inspect_fastmcp(mcp)
+ json_bytes = await format_fastmcp_info(info)
+
+ # Verify it's valid JSON
+ import json
+
+ data = json.loads(json_bytes)
+
+ # Check FastMCP-specific fields are present
+ assert "server" in data
+ assert data["server"]["name"] == "TestServer"
+ assert data["server"]["instructions"] == "Test instructions"
+ assert data["server"]["generation"] == 2 # v2 server
+ assert data["server"]["version"] == "1.2.3"
+ assert "capabilities" in data["server"]
+
+ # Check environment information
+ assert "environment" in data
+ assert data["environment"]["fastmcp"] == fastmcp.__version__
+ assert data["environment"]["mcp"] == importlib.metadata.version("mcp")
+
+ # Check tools
+ assert len(data["tools"]) == 1
+ assert data["tools"][0]["name"] == "test_tool"
+ assert data["tools"][0]["enabled"] is True
+ assert "tags" in data["tools"][0]
+
+ async def test_format_mcp_info(self):
+ """Test formatting as MCP protocol JSON."""
+ mcp = FastMCP("TestServer", instructions="Test instructions", version="2.0.0")
+
+ @mcp.tool
+ def add(a: int, b: int) -> int:
+ """Add two numbers."""
+ return a + b
+
+ @mcp.prompt
+ def test_prompt(name: str) -> list:
+ """Test prompt."""
+ return [{"role": "user", "content": f"Hello {name}"}]
+
+ json_bytes = await format_mcp_info(mcp)
+
+ # Verify it's valid JSON
+ import json
+
+ data = json.loads(json_bytes)
+
+ # Check MCP protocol structure with camelCase
+ assert "serverInfo" in data
+ assert data["serverInfo"]["name"] == "TestServer"
+
+ # Check server version in MCP format
+ assert data["serverInfo"]["version"] == "2.0.0"
+
+ # MCP format SHOULD have environment fields
+ assert "environment" in data
+ assert data["environment"]["fastmcp"] == fastmcp.__version__
+ assert data["environment"]["mcp"] == importlib.metadata.version("mcp")
+ assert "capabilities" in data
+
+ assert "tools" in data
+ assert "prompts" in data
+ assert "resources" in data
+ assert "resourceTemplates" in data
+
+ # Check tools have MCP format (camelCase fields)
+ assert len(data["tools"]) == 1
+ assert data["tools"][0]["name"] == "add"
+ assert "inputSchema" in data["tools"][0]
+
+ # FastMCP-specific fields should not be present
+ assert "tags" not in data["tools"][0]
+ assert "enabled" not in data["tools"][0]
+
+ async def test_format_info_with_fastmcp_format(self):
+ """Test format_info with fastmcp format."""
+ mcp = FastMCP("TestServer")
+
+ @mcp.tool
+ def test() -> str:
+ return "test"
+
+ # Test with string format
+ json_bytes = await format_info(mcp, "fastmcp")
+ import json
+
+ data = json.loads(json_bytes)
+ assert data["server"]["name"] == "TestServer"
+ assert "tags" in data["tools"][0] # FastMCP-specific field
+
+ # Test with enum format
+ json_bytes = await format_info(mcp, InspectFormat.FASTMCP)
+ data = json.loads(json_bytes)
+ assert data["server"]["name"] == "TestServer"
+
+ async def test_format_info_with_mcp_format(self):
+ """Test format_info with mcp format."""
+ mcp = FastMCP("TestServer")
+
+ @mcp.tool
+ def test() -> str:
+ return "test"
+
+ json_bytes = await format_info(mcp, "mcp")
+
+ import json
+
+ data = json.loads(json_bytes)
+ assert "serverInfo" in data
+ assert "tools" in data
+ assert "inputSchema" in data["tools"][0] # MCP uses camelCase
+
+ async def test_format_info_requires_format(self):
+ """Test that format_info requires a format parameter."""
+ mcp = FastMCP("TestServer")
+
+ @mcp.tool
+ def test() -> str:
+ return "test"
+
+ # Should work with valid formats
+ json_bytes = await format_info(mcp, "fastmcp")
+ assert json_bytes
+
+ json_bytes = await format_info(mcp, "mcp")
+ assert json_bytes
+
+ # Should fail with invalid format
+ import pytest
+
+ with pytest.raises(ValueError, match="not a valid InspectFormat"):
+ await format_info(mcp, "invalid") # type: ignore
+
+ async def test_tool_with_output_schema(self):
+ """Test that output_schema is properly extracted and included."""
+ mcp = FastMCP("TestServer")
+
+ @mcp.tool(
+ output_schema={
+ "type": "object",
+ "properties": {
+ "result": {"type": "number"},
+ "message": {"type": "string"},
+ },
+ }
+ )
+ def compute(x: int) -> dict:
+ """Compute something."""
+ return {"result": x * 2, "message": f"Doubled {x}"}
+
+ info = await inspect_fastmcp(mcp)
+
+ # Check output_schema is captured
+ assert len(info.tools) == 1
+ assert info.tools[0].output_schema is not None
+ assert info.tools[0].output_schema["type"] == "object"
+ assert "result" in info.tools[0].output_schema["properties"]
+
+ # Verify it's included in FastMCP format
+ json_bytes = await format_fastmcp_info(info)
+ import json
+
+ data = json.loads(json_bytes)
+ # Tools are at the top level, not nested
+ assert data["tools"][0]["output_schema"]["type"] == "object"