diff --git a/.github/workflows/marvin.yml b/.github/workflows/marvin.yml
index 65d8cbf8a..51674e615 100644
--- a/.github/workflows/marvin.yml
+++ b/.github/workflows/marvin.yml
@@ -44,11 +44,10 @@ jobs:
- name: Install dependencies
run: uv sync --python 3.12
- # Install pre-commit hooks automatically
- - name: Install pre-commit hooks
- run: |
- uv run pre-commit install
- echo "✅ Pre-commit hooks installed"
+ - name: Run prek
+ uses: j178/prek-action@v1
+ env:
+ SKIP: no-commit-to-branch
- name: Generate Marvin App token
id: marvin-token
diff --git a/.github/workflows/run-static.yml b/.github/workflows/run-static.yml
index ad95bcc54..2551c49b1 100644
--- a/.github/workflows/run-static.yml
+++ b/.github/workflows/run-static.yml
@@ -48,7 +48,7 @@ jobs:
exit 1
fi
echo "✅ Lockfile is up to date"
- - name: Run pre-commit
- run: uv run pre-commit run --all-files
+ - name: Run prek
+ uses: j178/prek-action@v1
env:
SKIP: no-commit-to-branch
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index d8cd826a6..f019b7185 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -79,10 +79,10 @@ jobs:
run: uv sync --resolution lowest-direct
- name: Run tests (excluding integration and client_process)
- run: uv run pytest --inline-snapshot=disable tests -m "not integration and not client_process" --numprocesses auto --maxprocesses 4 --dist worksteal
+ run: uv run --resolution lowest-direct pytest --inline-snapshot=disable tests -m "not integration and not client_process" --numprocesses auto --maxprocesses 4 --dist worksteal
- name: Run client process tests separately
- run: uv run pytest --inline-snapshot=disable tests -m "client_process" -x
+ run: uv run --resolution lowest-direct pytest --inline-snapshot=disable tests -m "client_process" -x
run_integration_tests:
name: "Run integration tests"
diff --git a/AGENTS.md b/AGENTS.md
index fa49d1429..9279aed35 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -10,7 +10,7 @@ FastMCP is a comprehensive Python framework (Python ≥3.10) for building Model
```bash
uv sync # Install dependencies
-uv run pre-commit run --all-files # Ruff + Prettier + ty
+uv run prek run --all-files # Ruff + Prettier + ty
uv run pytest # Run full test suite
```
@@ -100,12 +100,12 @@ async with Client(transport=StreamableHttpTransport(server_url)) as client:
### Git & CI
-- Pre-commit hooks are required (run automatically on commits)
-- Never amend commits to fix pre-commit failures
+- Prek hooks are required (run automatically on commits)
+- Never amend commits to fix prek failures
- Apply PR labels: bugs/breaking/enhancements/features
- Improvements = enhancements (not features) unless specified
- **NEVER** force-push on collaborative repos
-- **ALWAYS** run pre-commit before PRs
+- **ALWAYS** run prek before PRs
### Commit Messages and Agent Attribution
@@ -217,7 +217,7 @@ If something needs work, your review should help it get there through specific,
Before approving, verify:
-- [ ] All required development workflow steps completed (uv sync, pre-commit, pytest)
+- [ ] All required development workflow steps completed (uv sync, prek, pytest)
- [ ] Changes align with repository patterns and conventions
- [ ] API changes are documented and backwards-compatible where possible
- [ ] Error handling follows project patterns (specific exception types)
@@ -237,7 +237,7 @@ uv sync # Installs all deps including dev tools
- **Linting**: `uv run ruff check` (or with `--fix`)
- **Type Checking**: `uv run ty check`
-- **All Checks**: `uv run pre-commit run --all-files`
+- **All Checks**: `uv run prek run --all-files`
### Testing
@@ -260,6 +260,6 @@ uv sync # Installs all deps including dev tools
### Build Issues (Common Solutions)
1. **Dependencies**: Always `uv sync` first
-2. **Pre-commit fails**: Run `uv run pre-commit run --all-files` to see failures
+2. **Prek fails**: Run `uv run prek run --all-files` to see failures
3. **Type errors**: Use `uv run ty check` directly, check `pyproject.toml` config
4. **Test timeouts**: Default 5s - optimize or mark as integration tests
diff --git a/README.md b/README.md
index 977aaae15..ca00ef3d0 100644
--- a/README.md
+++ b/README.md
@@ -143,6 +143,8 @@ uv pip install fastmcp
For full installation instructions, including verification, upgrading from the official MCPSDK, and developer setup, see the [**Installation Guide**](https://gofastmcp.com/getting-started/installation).
+**Dependency Licensing:** FastMCP depends on Cyclopts for CLI functionality. Cyclopts v4 includes docutils as a transitive dependency, which has complex licensing that may trigger compliance reviews in some organizations. If this is a concern, you can install Cyclopts v5 alpha (`pip install "cyclopts>=5.0.0a1"`) which removes this dependency, or wait for the stable v5 release. See [this issue](https://github.com/BrianPugh/cyclopts/issues/672) for details.
+
## Core Concepts
These are the building blocks for creating MCP servers and clients with FastMCP.
@@ -481,20 +483,20 @@ uv run pytest --cov=src --cov=examples --cov-report=html
### Static Checks
-FastMCP uses `pre-commit` for code formatting, linting, and type-checking. All PRs must pass these checks (they run automatically in CI).
+FastMCP uses `prek` for code formatting, linting, and type-checking. All PRs must pass these checks (they run automatically in CI).
Install the hooks locally:
```bash
-uv run pre-commit install
+uv run prek install
```
The hooks will now run automatically on `git commit`. You can also run them manually at any time:
```bash
-pre-commit run --all-files
+prek run --all-files
# or via uv
-uv run pre-commit run --all-files
+uv run prek run --all-files
```
### Pull Requests
@@ -502,7 +504,7 @@ uv run pre-commit run --all-files
1. Fork the repository on GitHub.
2. Create a feature branch from `main`.
3. Make your changes, including tests and documentation updates.
-4. Ensure tests and pre-commit hooks pass.
+4. Ensure tests and prek hooks pass.
5. Commit your changes and push to your fork.
6. Open a pull request against the `main` branch of `jlowin/fastmcp`.
diff --git a/docs/clients/client.mdx b/docs/clients/client.mdx
index 0f5dc0f1e..36f300030 100644
--- a/docs/clients/client.mdx
+++ b/docs/clients/client.mdx
@@ -223,6 +223,55 @@ async with client:
print("Server is reachable")
```
+### Initialization and Server Information
+
+When you enter the client context manager, the client automatically performs an MCP initialization handshake with the server. This handshake exchanges capabilities, server metadata, and instructions. The result is available through the `initialize_result` property.
+
+```python
+from fastmcp import Client, FastMCP
+
+mcp = FastMCP(name="MyServer", instructions="Use the greet tool to say hello!")
+
+@mcp.tool
+def greet(name: str) -> str:
+ """Greet a user by name."""
+ return f"Hello, {name}!"
+
+async with Client(mcp) as client:
+ # Initialization already happened automatically
+ print(f"Server: {client.initialize_result.serverInfo.name}")
+ print(f"Version: {client.initialize_result.serverInfo.version}")
+ print(f"Instructions: {client.initialize_result.instructions}")
+ print(f"Capabilities: {client.initialize_result.capabilities.tools}")
+```
+
+#### Manual Initialization Control
+
+In advanced scenarios, you might want precise control over when initialization happens. For example, you may need custom error handling, want to defer initialization until after other setup, or need to measure initialization timing separately.
+
+Disable automatic initialization and call `initialize()` manually:
+
+```python
+from fastmcp import Client
+
+# Disable automatic initialization
+client = Client("my_mcp_server.py", auto_initialize=False)
+
+async with client:
+ # Connection established, but not initialized yet
+ print(f"Connected: {client.is_connected()}")
+ print(f"Initialized: {client.initialize_result is not None}") # False
+
+ # Initialize manually with custom timeout
+ result = await client.initialize(timeout=10.0)
+ print(f"Server: {result.serverInfo.name}")
+
+ # Now ready for operations
+ tools = await client.list_tools()
+```
+
+The `initialize()` method is idempotent - calling it multiple times returns the cached result from the first successful call.
+
## Client Configuration
Clients can be configured with additional handlers and settings for specialized use cases.
diff --git a/docs/clients/tools.mdx b/docs/clients/tools.mdx
index 58d0c0d28..372905bc5 100644
--- a/docs/clients/tools.mdx
+++ b/docs/clients/tools.mdx
@@ -101,6 +101,31 @@ async with client:
- `arguments`: Dictionary of arguments to pass to the tool (optional)
- `timeout`: Maximum execution time in seconds (optional, overrides client-level timeout)
- `progress_handler`: Progress callback function (optional, overrides client-level handler)
+- `meta`: Dictionary of metadata to send with the request (optional, see below)
+
+## Sending Metadata
+
+
+
+The `meta` parameter sends ancillary information alongside tool calls. This can be used for various purposes like observability, debugging, client identification, or any context the server may need beyond the tool's primary arguments.
+
+```python
+async with client:
+ result = await client.call_tool(
+ name="send_email",
+ arguments={
+ "to": "user@example.com",
+ "subject": "Hello",
+ "body": "Welcome!"
+ },
+ meta={
+ "trace_id": "abc-123",
+ "request_source": "mobile_app"
+ }
+ )
+```
+
+The structure and usage of `meta` is determined by your application. See [Client Metadata](/servers/context#client-metadata) in the server documentation to learn how to access this data in your tool implementations.
## Handling Results
diff --git a/docs/development/contributing.mdx b/docs/development/contributing.mdx
index c97dcf565..6be0ffba6 100644
--- a/docs/development/contributing.mdx
+++ b/docs/development/contributing.mdx
@@ -49,13 +49,13 @@ cd fastmcp
# Install all dependencies including dev tools
uv sync
-# Install pre-commit hooks
-uv run pre-commit install
+# Install prek hooks
+uv run prek install
```
In addition, some development commands require [just](https://github.com/casey/just) to be installed.
-Pre-commit hooks will run automatically on every commit to catch issues before they reach CI. If you see failures, fix them before committing - never commit broken code expecting to fix it later.
+Prek hooks will run automatically on every commit to catch issues before they reach CI. If you see failures, fix them before committing - never commit broken code expecting to fix it later.
### Development Standards
@@ -100,19 +100,19 @@ The focus is on idiomatic, high-quality Python. FastMCP uses patterns like `NotS
**Breaking established patterns** confuses readers. If you must deviate, discuss in the issue first.
-### Pre-Commit Checks
+### Prek Checks
```bash
# Runs automatically on commit, or manually:
-uv run pre-commit run --all-files
+uv run prek run --all-files
```
This runs three critical tools:
- **Ruff**: Linting and formatting
-- **ty**: Static type checking
-- **Pytest**: Core test suite
+- **Prettier**: Code formatting
+- **ty**: Static type checking
-CI will reject PRs that fail these checks. Always run them locally first.
+Pytest runs separately as a distinct workflow step after prek checks pass. CI will reject PRs that fail these checks. Always run them locally first.
### Testing
@@ -155,7 +155,7 @@ just api-ref-all
#### Before Submitting
-1. **Run all checks**: `uv run pre-commit run --all-files && uv run pytest`
+1. **Run all checks**: `uv run prek run --all-files && uv run pytest`
2. **Keep scope small**: One feature or fix per PR
3. **Write clear description**: Your PR description becomes permanent documentation
4. **Update docs**: Include documentation for API changes
diff --git a/docs/docs.json b/docs/docs.json
index cac05f32e..ecb10a6fd 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -355,6 +355,7 @@
"python-sdk/fastmcp-server-auth-providers-auth0",
"python-sdk/fastmcp-server-auth-providers-aws",
"python-sdk/fastmcp-server-auth-providers-azure",
+ "python-sdk/fastmcp-server-auth-providers-debug",
"python-sdk/fastmcp-server-auth-providers-descope",
"python-sdk/fastmcp-server-auth-providers-github",
"python-sdk/fastmcp-server-auth-providers-google",
diff --git a/docs/getting-started/installation.mdx b/docs/getting-started/installation.mdx
index 8da0df91c..edcaa5983 100644
--- a/docs/getting-started/installation.mdx
+++ b/docs/getting-started/installation.mdx
@@ -42,6 +42,20 @@ Python version: 3.12.2
Platform: macOS-15.3.1-arm64-arm-64bit
FastMCP root path: ~/Developer/fastmcp
```
+
+### Dependency Licensing
+
+
+FastMCP depends on Cyclopts for CLI functionality. Cyclopts v4 includes docutils as a transitive dependency, which has complex licensing that may trigger compliance reviews in some organizations.
+
+If this is a concern, you can install Cyclopts v5 alpha which removes this dependency:
+
+```bash
+pip install "cyclopts>=5.0.0a1"
+```
+
+Alternatively, wait for the stable v5 release. See [this issue](https://github.com/BrianPugh/cyclopts/issues/672) for details.
+
## Upgrading from the Official MCP SDK
Upgrading from the official MCP SDK's FastMCP 1.0 to FastMCP 2.0 is generally straightforward. The core server API is highly compatible, and in many cases, changing your import statement from `from mcp.server.fastmcp import FastMCP` to `from fastmcp import FastMCP` will be sufficient.
diff --git a/docs/python-sdk/fastmcp-cli-cli.mdx b/docs/python-sdk/fastmcp-cli-cli.mdx
index b5c662725..3b6f0bf3a 100644
--- a/docs/python-sdk/fastmcp-cli-cli.mdx
+++ b/docs/python-sdk/fastmcp-cli-cli.mdx
@@ -105,7 +105,7 @@ fastmcp inspect # auto-detect fastmcp.json
- `server_spec`: Python file to inspect, optionally with \:object suffix, or fastmcp.json
-### `prepare`
+### `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
diff --git a/docs/python-sdk/fastmcp-cli-install-cursor.mdx b/docs/python-sdk/fastmcp-cli-install-cursor.mdx
index 0dc106747..0463c3a05 100644
--- a/docs/python-sdk/fastmcp-cli-install-cursor.mdx
+++ b/docs/python-sdk/fastmcp-cli-install-cursor.mdx
@@ -10,7 +10,7 @@ Cursor integration for FastMCP install using Cyclopts.
## Functions
-### `generate_cursor_deeplink`
+### `generate_cursor_deeplink`
```python
generate_cursor_deeplink(server_name: str, server_config: StdioMCPServer) -> str
@@ -27,7 +27,7 @@ Generate a Cursor deeplink for installing the MCP server.
- Deeplink URL that can be clicked to install the server
-### `open_deeplink`
+### `open_deeplink`
```python
open_deeplink(deeplink: str) -> bool
@@ -43,7 +43,7 @@ Attempt to open a deeplink URL using the system's default handler.
- True if the command succeeded, False otherwise
-### `install_cursor_workspace`
+### `install_cursor_workspace`
```python
install_cursor_workspace(file: Path, server_object: str | None, name: str, workspace_path: Path) -> bool
@@ -68,7 +68,7 @@ Install FastMCP server to workspace-specific Cursor configuration.
- True if installation was successful, False otherwise
-### `install_cursor`
+### `install_cursor`
```python
install_cursor(file: Path, server_object: str | None, name: str) -> bool
@@ -93,7 +93,7 @@ Install FastMCP server in Cursor.
- True if installation was successful, False otherwise
-### `cursor_command`
+### `cursor_command`
```python
cursor_command(server_spec: str) -> None
diff --git a/docs/python-sdk/fastmcp-client-auth-oauth.mdx b/docs/python-sdk/fastmcp-client-auth-oauth.mdx
index cb1f6ba77..6da25abbf 100644
--- a/docs/python-sdk/fastmcp-client-auth-oauth.mdx
+++ b/docs/python-sdk/fastmcp-client-auth-oauth.mdx
@@ -7,7 +7,7 @@ sidebarTitle: oauth
## Functions
-### `check_if_auth_required`
+### `check_if_auth_required`
```python
check_if_auth_required(mcp_url: str, httpx_kwargs: dict[str, Any] | None = None) -> bool
@@ -28,41 +28,41 @@ Check if the MCP endpoint requires authentication by making a test request.
Raised when OAuth client credentials are not found on the server.
-### `TokenStorageAdapter`
+### `TokenStorageAdapter`
**Methods:**
-#### `clear`
+#### `clear`
```python
clear(self) -> None
```
-#### `get_tokens`
+#### `get_tokens`
```python
get_tokens(self) -> OAuthToken | None
```
-#### `set_tokens`
+#### `set_tokens`
```python
set_tokens(self, tokens: OAuthToken) -> None
```
-#### `get_client_info`
+#### `get_client_info`
```python
get_client_info(self) -> OAuthClientInformationFull | None
```
-#### `set_client_info`
+#### `set_client_info`
```python
set_client_info(self, client_info: OAuthClientInformationFull) -> None
```
-### `OAuth`
+### `OAuth`
OAuth client provider for MCP servers with browser-based authentication.
@@ -91,7 +91,7 @@ callback_handler(self) -> tuple[str, str | None]
Handle OAuth callback and return (auth_code, state).
-#### `async_auth_flow`
+#### `async_auth_flow`
```python
async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.Request, httpx.Response]
diff --git a/docs/python-sdk/fastmcp-client-client.mdx b/docs/python-sdk/fastmcp-client-client.mdx
index e089c8201..1b915ca6f 100644
--- a/docs/python-sdk/fastmcp-client-client.mdx
+++ b/docs/python-sdk/fastmcp-client-client.mdx
@@ -7,7 +7,7 @@ sidebarTitle: client
## Classes
-### `ClientSessionState`
+### `ClientSessionState`
Holds all session-related state for a Client instance.
@@ -16,7 +16,7 @@ This allows clean separation of configuration (which is copied) from
session state (which should be fresh for each new client instance).
-### `Client`
+### `Client`
MCP client that delegates connection management to a Transport instance.
@@ -79,7 +79,7 @@ async with client:
**Methods:**
-#### `session`
+#### `session`
```python
session(self) -> ClientSession
@@ -88,16 +88,16 @@ session(self) -> ClientSession
Get the current active session. Raises RuntimeError if not connected.
-#### `initialize_result`
+#### `initialize_result`
```python
-initialize_result(self) -> mcp.types.InitializeResult
+initialize_result(self) -> mcp.types.InitializeResult | None
```
Get the result of the initialization request.
-#### `set_roots`
+#### `set_roots`
```python
set_roots(self, roots: RootsList | RootsHandler) -> None
@@ -106,7 +106,7 @@ set_roots(self, roots: RootsList | RootsHandler) -> None
Set the roots for the client. This does not automatically call `send_roots_list_changed`.
-#### `set_sampling_callback`
+#### `set_sampling_callback`
```python
set_sampling_callback(self, sampling_callback: ClientSamplingHandler) -> None
@@ -115,7 +115,7 @@ set_sampling_callback(self, sampling_callback: ClientSamplingHandler) -> None
Set the sampling callback for the client.
-#### `set_elicitation_callback`
+#### `set_elicitation_callback`
```python
set_elicitation_callback(self, elicitation_callback: ElicitationHandler) -> None
@@ -124,7 +124,7 @@ set_elicitation_callback(self, elicitation_callback: ElicitationHandler) -> None
Set the elicitation callback for the client.
-#### `is_connected`
+#### `is_connected`
```python
is_connected(self) -> bool
@@ -133,7 +133,7 @@ is_connected(self) -> bool
Check if the client is currently connected.
-#### `new`
+#### `new`
```python
new(self) -> Client[ClientTransportT]
@@ -155,7 +155,35 @@ share state with the original client.
close(self)
```
-#### `ping`
+#### `initialize`
+
+```python
+initialize(self, timeout: datetime.timedelta | float | int | None = None) -> mcp.types.InitializeResult
+```
+
+Send an initialize request to the server.
+
+This method performs the MCP initialization handshake with the server,
+exchanging capabilities and server information. It is idempotent - calling
+it multiple times returns the cached result from the first call.
+
+The initialization happens automatically when entering the client context
+manager unless `auto_initialize=False` was set during client construction.
+Manual calls to this method are only needed when auto-initialization is disabled.
+
+**Args:**
+- `timeout`: Optional timeout for the initialization request (seconds or timedelta).
+If None, uses the client's init_timeout setting.
+
+**Returns:**
+- The server's initialization response containing server info,
+capabilities, protocol version, and optional instructions.
+
+**Raises:**
+- `RuntimeError`: If the client is not connected or initialization times out.
+
+
+#### `ping`
```python
ping(self) -> bool
@@ -164,7 +192,7 @@ ping(self) -> bool
Send a ping request.
-#### `cancel`
+#### `cancel`
```python
cancel(self, request_id: str | int, reason: str | None = None) -> None
@@ -173,7 +201,7 @@ cancel(self, request_id: str | int, reason: str | None = None) -> None
Send a cancellation notification for an in-progress request.
-#### `progress`
+#### `progress`
```python
progress(self, progress_token: str | int, progress: float, total: float | None = None, message: str | None = None) -> None
@@ -182,7 +210,7 @@ progress(self, progress_token: str | int, progress: float, total: float | None =
Send a progress notification.
-#### `set_logging_level`
+#### `set_logging_level`
```python
set_logging_level(self, level: mcp.types.LoggingLevel) -> None
@@ -191,7 +219,7 @@ set_logging_level(self, level: mcp.types.LoggingLevel) -> None
Send a logging/setLevel request.
-#### `send_roots_list_changed`
+#### `send_roots_list_changed`
```python
send_roots_list_changed(self) -> None
@@ -200,7 +228,7 @@ send_roots_list_changed(self) -> None
Send a roots/list_changed notification.
-#### `list_resources_mcp`
+#### `list_resources_mcp`
```python
list_resources_mcp(self) -> mcp.types.ListResourcesResult
@@ -216,7 +244,7 @@ containing the list of resources and any additional metadata.
- `RuntimeError`: If called while the client is not connected.
-#### `list_resources`
+#### `list_resources`
```python
list_resources(self) -> list[mcp.types.Resource]
@@ -231,7 +259,7 @@ Retrieve a list of resources available on the server.
- `RuntimeError`: If called while the client is not connected.
-#### `list_resource_templates_mcp`
+#### `list_resource_templates_mcp`
```python
list_resource_templates_mcp(self) -> mcp.types.ListResourceTemplatesResult
@@ -247,7 +275,7 @@ containing the list of resource templates and any additional metadata.
- `RuntimeError`: If called while the client is not connected.
-#### `list_resource_templates`
+#### `list_resource_templates`
```python
list_resource_templates(self) -> list[mcp.types.ResourceTemplate]
@@ -262,7 +290,7 @@ Retrieve a list of resource templates available on the server.
- `RuntimeError`: If called while the client is not connected.
-#### `read_resource_mcp`
+#### `read_resource_mcp`
```python
read_resource_mcp(self, uri: AnyUrl | str) -> mcp.types.ReadResourceResult
@@ -281,7 +309,7 @@ containing the resource contents and any additional metadata.
- `RuntimeError`: If called while the client is not connected.
-#### `read_resource`
+#### `read_resource`
```python
read_resource(self, uri: AnyUrl | str) -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]
@@ -300,7 +328,7 @@ objects, typically containing either text or binary data.
- `RuntimeError`: If called while the client is not connected.
-#### `list_prompts_mcp`
+#### `list_prompts_mcp`
```python
list_prompts_mcp(self) -> mcp.types.ListPromptsResult
@@ -316,7 +344,7 @@ containing the list of prompts and any additional metadata.
- `RuntimeError`: If called while the client is not connected.
-#### `list_prompts`
+#### `list_prompts`
```python
list_prompts(self) -> list[mcp.types.Prompt]
@@ -331,7 +359,7 @@ Retrieve a list of prompts available on the server.
- `RuntimeError`: If called while the client is not connected.
-#### `get_prompt_mcp`
+#### `get_prompt_mcp`
```python
get_prompt_mcp(self, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.GetPromptResult
@@ -351,7 +379,7 @@ containing the prompt messages and any additional metadata.
- `RuntimeError`: If called while the client is not connected.
-#### `get_prompt`
+#### `get_prompt`
```python
get_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.GetPromptResult
@@ -371,7 +399,7 @@ containing the prompt messages and any additional metadata.
- `RuntimeError`: If called while the client is not connected.
-#### `complete_mcp`
+#### `complete_mcp`
```python
complete_mcp(self, ref: mcp.types.ResourceTemplateReference | mcp.types.PromptReference, argument: dict[str, str], context_arguments: dict[str, Any] | None = None) -> mcp.types.CompleteResult
@@ -393,7 +421,7 @@ containing the completion and any additional metadata.
- `RuntimeError`: If called while the client is not connected.
-#### `complete`
+#### `complete`
```python
complete(self, ref: mcp.types.ResourceTemplateReference | mcp.types.PromptReference, argument: dict[str, str], context_arguments: dict[str, Any] | None = None) -> mcp.types.Completion
@@ -414,7 +442,7 @@ include with the completion request. Defaults to None.
- `RuntimeError`: If called while the client is not connected.
-#### `list_tools_mcp`
+#### `list_tools_mcp`
```python
list_tools_mcp(self) -> mcp.types.ListToolsResult
@@ -430,7 +458,7 @@ containing the list of tools and any additional metadata.
- `RuntimeError`: If called while the client is not connected.
-#### `list_tools`
+#### `list_tools`
```python
list_tools(self) -> list[mcp.types.Tool]
@@ -445,7 +473,7 @@ Retrieve a list of tools available on the server.
- `RuntimeError`: If called while the client is not connected.
-#### `call_tool_mcp`
+#### `call_tool_mcp`
```python
call_tool_mcp(self, name: str, arguments: dict[str, Any], progress_handler: ProgressHandler | None = None, timeout: datetime.timedelta | float | int | None = None) -> mcp.types.CallToolResult
@@ -470,7 +498,7 @@ containing the tool result and any additional metadata.
- `RuntimeError`: If called while the client is not connected.
-#### `call_tool`
+#### `call_tool`
```python
call_tool(self, name: str, arguments: dict[str, Any] | None = None, timeout: datetime.timedelta | float | int | None = None, progress_handler: ProgressHandler | None = None, raise_on_error: bool = True) -> CallToolResult
@@ -500,10 +528,10 @@ raw result object.
- `RuntimeError`: If called while the client is not connected.
-#### `generate_name`
+#### `generate_name`
```python
generate_name(cls, name: str | None = None) -> str
```
-### `CallToolResult`
+### `CallToolResult`
diff --git a/docs/python-sdk/fastmcp-client-transports.mdx b/docs/python-sdk/fastmcp-client-transports.mdx
index 9584e4d2e..acc5aca73 100644
--- a/docs/python-sdk/fastmcp-client-transports.mdx
+++ b/docs/python-sdk/fastmcp-client-transports.mdx
@@ -102,7 +102,7 @@ close(self)
Close the transport.
-### `WSTransport`
+### `WSTransport`
Transport implementation that connects to an MCP server via WebSockets.
@@ -110,13 +110,13 @@ Transport implementation that connects to an MCP server via WebSockets.
**Methods:**
-#### `connect_session`
+#### `connect_session`
```python
connect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession]
```
-### `SSETransport`
+### `SSETransport`
Transport implementation that connects to an MCP server via Server-Sent Events.
@@ -124,13 +124,13 @@ Transport implementation that connects to an MCP server via Server-Sent Events.
**Methods:**
-#### `connect_session`
+#### `connect_session`
```python
connect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession]
```
-### `StreamableHttpTransport`
+### `StreamableHttpTransport`
Transport implementation that connects to an MCP server via Streamable HTTP Requests.
@@ -138,13 +138,13 @@ Transport implementation that connects to an MCP server via Streamable HTTP Requ
**Methods:**
-#### `connect_session`
+#### `connect_session`
```python
connect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession]
```
-### `StdioTransport`
+### `StdioTransport`
Base transport for connecting to an MCP server via subprocess with stdio.
@@ -155,67 +155,67 @@ transports like Python, Node, Uvx, etc.
**Methods:**
-#### `connect_session`
+#### `connect_session`
```python
connect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession]
```
-#### `connect`
+#### `connect`
```python
connect(self, **session_kwargs: Unpack[SessionKwargs]) -> ClientSession | None
```
-#### `disconnect`
+#### `disconnect`
```python
disconnect(self)
```
-#### `close`
+#### `close`
```python
close(self)
```
-### `PythonStdioTransport`
+### `PythonStdioTransport`
Transport for running Python scripts.
-### `FastMCPStdioTransport`
+### `FastMCPStdioTransport`
Transport for running FastMCP servers using the FastMCP CLI.
-### `NodeStdioTransport`
+### `NodeStdioTransport`
Transport for running Node.js scripts.
-### `UvStdioTransport`
+### `UvStdioTransport`
Transport for running commands via the uv tool.
-### `UvxStdioTransport`
+### `UvxStdioTransport`
Transport for running commands via the uvx tool.
-### `NpxStdioTransport`
+### `NpxStdioTransport`
Transport for running commands via the npx tool.
-### `FastMCPTransport`
+### `FastMCPTransport`
In-memory transport for FastMCP servers.
@@ -228,7 +228,7 @@ tests or scenarios where client and server run in the same runtime.
**Methods:**
-#### `connect_session`
+#### `connect_session`
```python
connect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession]
diff --git a/docs/python-sdk/fastmcp-mcp_config.mdx b/docs/python-sdk/fastmcp-mcp_config.mdx
index 805427bb5..cd4356e45 100644
--- a/docs/python-sdk/fastmcp-mcp_config.mdx
+++ b/docs/python-sdk/fastmcp-mcp_config.mdx
@@ -42,7 +42,7 @@ infer_transport_type_from_url(url: str | AnyUrl) -> Literal['http', 'sse']
Infer the appropriate transport type from the given URL.
-### `update_config_file`
+### `update_config_file`
```python
update_config_file(file_path: Path, server_name: str, server_config: CanonicalMCPServerTypes) -> None
@@ -167,7 +167,7 @@ from_file(cls, file_path: Path) -> Self
Load configuration from JSON file.
-### `CanonicalMCPConfig`
+### `CanonicalMCPConfig`
Canonical MCP configuration format.
@@ -178,7 +178,7 @@ The format is designed to be client-agnostic and extensible for future use cases
**Methods:**
-#### `add_server`
+#### `add_server`
```python
add_server(self, name: str, server: CanonicalMCPServerTypes) -> None
diff --git a/docs/python-sdk/fastmcp-prompts-prompt.mdx b/docs/python-sdk/fastmcp-prompts-prompt.mdx
index 67856571d..b93053e5d 100644
--- a/docs/python-sdk/fastmcp-prompts-prompt.mdx
+++ b/docs/python-sdk/fastmcp-prompts-prompt.mdx
@@ -107,7 +107,7 @@ The function can return:
- A sequence of any of the above
-#### `render`
+#### `render`
```python
render(self, arguments: dict[str, Any] | None = None) -> list[PromptMessage]
diff --git a/docs/python-sdk/fastmcp-server-auth-oauth_proxy.mdx b/docs/python-sdk/fastmcp-server-auth-oauth_proxy.mdx
index 46925bc00..cc676e63e 100644
--- a/docs/python-sdk/fastmcp-server-auth-oauth_proxy.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-oauth_proxy.mdx
@@ -36,6 +36,26 @@ create_consent_html(client_id: str, redirect_uri: str, scopes: list[str], txn_id
Create a styled HTML consent page for OAuth authorization requests.
+### `create_error_html`
+
+```python
+create_error_html(error_title: str, error_message: str, error_details: dict[str, str] | None = None, server_name: str | None = None, server_icon_url: str | None = None) -> str
+```
+
+
+Create a styled HTML error page for OAuth errors.
+
+**Args:**
+- `error_title`: The error title (e.g., "OAuth Error", "Authorization Failed")
+- `error_message`: The main error message to display
+- `error_details`: Optional dictionary of error details to show (e.g., {"Error Code"\: "invalid_client"})
+- `server_name`: Optional server name to display
+- `server_icon_url`: Optional URL to server icon/logo
+
+**Returns:**
+- Complete HTML page as a string
+
+
## Classes
### `OAuthTransaction`
@@ -119,7 +139,7 @@ This is essential for cached token scenarios where the client may
reconnect with a different port.
-### `TokenHandler`
+### `TokenHandler`
TokenHandler that returns OAuth 2.1 compliant error responses.
@@ -142,7 +162,7 @@ Per MCP spec: "Invalid or expired tokens MUST receive a HTTP 401 response."
**Methods:**
-#### `response`
+#### `response`
```python
response(self, obj: TokenSuccessResponse | TokenErrorResponse)
@@ -151,7 +171,7 @@ response(self, obj: TokenSuccessResponse | TokenErrorResponse)
Override response method to provide OAuth 2.1 compliant error handling.
-### `OAuthProxy`
+### `OAuthProxy`
OAuth provider that presents a DCR-compliant interface while proxying to non-DCR IDPs.
@@ -261,7 +281,7 @@ Handles provider-specific requirements:
**Methods:**
-#### `get_client`
+#### `get_client`
```python
get_client(self, client_id: str) -> OAuthClientInformationFull | None
@@ -273,7 +293,7 @@ provided to the DCR client during registration, not the upstream client ID.
For unregistered clients, returns None (which will raise an error in the SDK).
-#### `register_client`
+#### `register_client`
```python
register_client(self, client_info: OAuthClientInformationFull) -> None
@@ -287,7 +307,7 @@ redirect URI will likely be localhost or unknown to the proxied IDP. The
proxied IDP only knows about this server's fixed redirect URI.
-#### `authorize`
+#### `authorize`
```python
authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str
@@ -304,7 +324,7 @@ If consent is disabled (require_authorization_consent=False), skip the consent s
and redirect directly to the upstream IdP.
-#### `load_authorization_code`
+#### `load_authorization_code`
```python
load_authorization_code(self, client: OAuthClientInformationFull, authorization_code: str) -> AuthorizationCode | None
@@ -316,7 +336,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
@@ -334,7 +354,7 @@ Implements the token factory pattern:
PKCE validation is handled by the MCP framework before this method is called.
-#### `load_refresh_token`
+#### `load_refresh_token`
```python
load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None
@@ -343,7 +363,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
@@ -360,7 +380,7 @@ Implements two-tier refresh:
6. Keep same FastMCP refresh token (unless upstream rotates)
-#### `load_access_token`
+#### `load_access_token`
```python
load_access_token(self, token: str) -> AccessToken | None
@@ -379,7 +399,7 @@ The FastMCP JWT is a reference token - all authorization data comes
from validating the upstream token via the TokenVerifier.
-#### `revoke_token`
+#### `revoke_token`
```python
revoke_token(self, token: AccessToken | RefreshToken) -> None
@@ -391,7 +411,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, mcp_path: str | None = None) -> list[Route]
diff --git a/docs/python-sdk/fastmcp-server-auth-oidc_proxy.mdx b/docs/python-sdk/fastmcp-server-auth-oidc_proxy.mdx
index 867f110fc..2aa18f10a 100644
--- a/docs/python-sdk/fastmcp-server-auth-oidc_proxy.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-oidc_proxy.mdx
@@ -52,7 +52,7 @@ that is OIDC compliant.
**Methods:**
-#### `get_oidc_configuration`
+#### `get_oidc_configuration`
```python
get_oidc_configuration(self, config_url: AnyHttpUrl, strict: bool | None, timeout_seconds: int | None) -> OIDCConfiguration
@@ -66,7 +66,7 @@ Gets the OIDC configuration for the specified configuration URL.
- `timeout_seconds`: HTTP request timeout in seconds
-#### `get_token_verifier`
+#### `get_token_verifier`
```python
get_token_verifier(self) -> TokenVerifier
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx b/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx
index 44f2d37c4..b0474eb58 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx
@@ -20,7 +20,7 @@ using the OAuth Proxy pattern for non-DCR OAuth flows.
Settings for Azure OAuth provider.
-### `AzureProvider`
+### `AzureProvider`
Azure (Microsoft Entra) OAuth provider for FastMCP.
@@ -55,7 +55,7 @@ Setup:
**Methods:**
-#### `authorize`
+#### `authorize`
```python
authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-debug.mdx b/docs/python-sdk/fastmcp-server-auth-providers-debug.mdx
new file mode 100644
index 000000000..1bf9c3175
--- /dev/null
+++ b/docs/python-sdk/fastmcp-server-auth-providers-debug.mdx
@@ -0,0 +1,70 @@
+---
+title: debug
+sidebarTitle: debug
+---
+
+# `fastmcp.server.auth.providers.debug`
+
+
+Debug token verifier for testing and special cases.
+
+This module provides a flexible token verifier that delegates validation
+to a custom callable. Useful for testing, development, or scenarios where
+standard verification isn't possible (like opaque tokens without introspection).
+
+Example:
+ ```python
+ from fastmcp import FastMCP
+ from fastmcp.server.auth.providers.debug import DebugTokenVerifier
+
+ # Accept all tokens (default - useful for testing)
+ auth = DebugTokenVerifier()
+
+ # Custom sync validation logic
+ auth = DebugTokenVerifier(validate=lambda token: token.startswith("valid-"))
+
+ # Custom async validation logic
+ async def check_cache(token: str) -> bool:
+ return await redis.exists(f"token:{token}")
+
+ auth = DebugTokenVerifier(validate=check_cache)
+
+ mcp = FastMCP("My Server", auth=auth)
+ ```
+
+
+## Classes
+
+### `DebugTokenVerifier`
+
+
+Token verifier with custom validation logic.
+
+This verifier delegates token validation to a user-provided callable.
+By default, it accepts all non-empty tokens (useful for testing).
+
+Use cases:
+- Testing: Accept any token without real verification
+- Development: Custom validation logic for prototyping
+- Opaque tokens: When you have tokens with no introspection endpoint
+
+WARNING: This bypasses standard security checks. Only use in controlled
+environments or when you understand the security implications.
+
+
+**Methods:**
+
+#### `verify_token`
+
+```python
+verify_token(self, token: str) -> AccessToken | None
+```
+
+Verify token using custom validation logic.
+
+**Args:**
+- `token`: The token string to validate
+
+**Returns:**
+- AccessToken if validation succeeds, None otherwise
+
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-jwt.mdx b/docs/python-sdk/fastmcp-server-auth-providers-jwt.mdx
index 345ec7b95..0c7cf0c93 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-jwt.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-jwt.mdx
@@ -94,16 +94,16 @@ Use this when:
load_access_token(self, token: str) -> AccessToken | None
```
-Validates the provided JWT bearer token.
+Validate a JWT bearer token and return an AccessToken when the token is valid.
**Args:**
-- `token`: The JWT token string to validate
+- `token`: The JWT bearer token string to validate.
**Returns:**
-- AccessToken object if valid, None if invalid or expired
+- AccessToken | None: An AccessToken populated from token claims if the token is valid; `None` if the token is expired, has an invalid signature or format, fails issuer/audience/scope validation, or any other validation error occurs.
-#### `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-context.mdx b/docs/python-sdk/fastmcp-server-context.mdx
index 3b0f0539d..dd186188c 100644
--- a/docs/python-sdk/fastmcp-server-context.mdx
+++ b/docs/python-sdk/fastmcp-server-context.mdx
@@ -362,7 +362,7 @@ type or dataclass or BaseModel. If it is a primitive type, an
object schema with a single "value" field will be generated.
-#### `get_http_request`
+#### `get_http_request`
```python
get_http_request(self) -> Request
@@ -371,7 +371,7 @@ get_http_request(self) -> Request
Get the active starlette request.
-#### `set_state`
+#### `set_state`
```python
set_state(self, key: str, value: Any) -> None
@@ -380,7 +380,7 @@ set_state(self, key: str, value: Any) -> None
Set a value in the context state.
-#### `get_state`
+#### `get_state`
```python
get_state(self, key: str) -> Any
diff --git a/docs/python-sdk/fastmcp-server-dependencies.mdx b/docs/python-sdk/fastmcp-server-dependencies.mdx
index d9a6d6bed..3c50f6421 100644
--- a/docs/python-sdk/fastmcp-server-dependencies.mdx
+++ b/docs/python-sdk/fastmcp-server-dependencies.mdx
@@ -7,19 +7,19 @@ sidebarTitle: dependencies
## Functions
-### `get_context`
+### `get_context`
```python
get_context() -> Context
```
-### `get_http_request`
+### `get_http_request`
```python
get_http_request() -> Request
```
-### `get_http_headers`
+### `get_http_headers`
```python
get_http_headers(include_all: bool = False) -> dict[str, str]
@@ -35,7 +35,7 @@ By default, strips problematic headers like `content-length` that cause issues i
If `include_all` is True, all headers are returned.
-### `get_access_token`
+### `get_access_token`
```python
get_access_token() -> AccessToken | None
diff --git a/docs/python-sdk/fastmcp-server-middleware-caching.mdx b/docs/python-sdk/fastmcp-server-middleware-caching.mdx
index a0c837507..929203e2f 100644
--- a/docs/python-sdk/fastmcp-server-middleware-caching.mdx
+++ b/docs/python-sdk/fastmcp-server-middleware-caching.mdx
@@ -46,63 +46,63 @@ unwrap(cls, values: Sequence[Self]) -> list[ReadResourceContents]
**Methods:**
-#### `wrap`
+#### `wrap`
```python
wrap(cls, value: ToolResult) -> Self
```
-#### `unwrap`
+#### `unwrap`
```python
unwrap(self) -> ToolResult
```
-### `SharedMethodSettings`
+### `SharedMethodSettings`
Shared config for a cache method.
-### `ListToolsSettings`
+### `ListToolsSettings`
Configuration options for Tool-related caching.
-### `ListResourcesSettings`
+### `ListResourcesSettings`
Configuration options for Resource-related caching.
-### `ListPromptsSettings`
+### `ListPromptsSettings`
Configuration options for Prompt-related caching.
-### `CallToolSettings`
+### `CallToolSettings`
Configuration options for Tool-related caching.
-### `ReadResourceSettings`
+### `ReadResourceSettings`
Configuration options for Resource-related caching.
-### `GetPromptSettings`
+### `GetPromptSettings`
Configuration options for Prompt-related caching.
-### `ResponseCachingStatistics`
+### `ResponseCachingStatistics`
-### `ResponseCachingMiddleware`
+### `ResponseCachingMiddleware`
The response caching middleware offers a simple way to cache responses to mcp methods. The Middleware
@@ -119,7 +119,7 @@ Notes:
**Methods:**
-#### `on_list_tools`
+#### `on_list_tools`
```python
on_list_tools(self, context: MiddlewareContext[mcp.types.ListToolsRequest], call_next: CallNext[mcp.types.ListToolsRequest, Sequence[Tool]]) -> Sequence[Tool]
@@ -129,7 +129,7 @@ List tools from the cache, if caching is enabled, and the result is in the cache
otherwise call the next middleware and store the result in the cache if caching is enabled.
-#### `on_list_resources`
+#### `on_list_resources`
```python
on_list_resources(self, context: MiddlewareContext[mcp.types.ListResourcesRequest], call_next: CallNext[mcp.types.ListResourcesRequest, Sequence[Resource]]) -> Sequence[Resource]
@@ -139,7 +139,7 @@ List resources from the cache, if caching is enabled, and the result is in the c
otherwise call the next middleware and store the result in the cache if caching is enabled.
-#### `on_list_prompts`
+#### `on_list_prompts`
```python
on_list_prompts(self, context: MiddlewareContext[mcp.types.ListPromptsRequest], call_next: CallNext[mcp.types.ListPromptsRequest, Sequence[Prompt]]) -> Sequence[Prompt]
@@ -149,7 +149,7 @@ List prompts from the cache, if caching is enabled, and the result is in the cac
otherwise call the next middleware and store the result in the cache if caching is enabled.
-#### `on_call_tool`
+#### `on_call_tool`
```python
on_call_tool(self, context: MiddlewareContext[mcp.types.CallToolRequestParams], call_next: CallNext[mcp.types.CallToolRequestParams, ToolResult]) -> ToolResult
@@ -159,7 +159,7 @@ Call a tool from the cache, if caching is enabled, and the result is in the cach
otherwise call the next middleware and store the result in the cache if caching is enabled.
-#### `on_read_resource`
+#### `on_read_resource`
```python
on_read_resource(self, context: MiddlewareContext[mcp.types.ReadResourceRequestParams], call_next: CallNext[mcp.types.ReadResourceRequestParams, Sequence[ReadResourceContents]]) -> Sequence[ReadResourceContents]
@@ -169,7 +169,7 @@ Read a resource from the cache, if caching is enabled, and the result is in the
otherwise call the next middleware and store the result in the cache if caching is enabled.
-#### `on_get_prompt`
+#### `on_get_prompt`
```python
on_get_prompt(self, context: MiddlewareContext[mcp.types.GetPromptRequestParams], call_next: CallNext[mcp.types.GetPromptRequestParams, mcp.types.GetPromptResult]) -> mcp.types.GetPromptResult
@@ -179,7 +179,7 @@ Get a prompt from the cache, if caching is enabled, and the result is in the cac
otherwise call the next middleware and store the result in the cache if caching is enabled.
-#### `statistics`
+#### `statistics`
```python
statistics(self) -> ResponseCachingStatistics
diff --git a/docs/python-sdk/fastmcp-server-middleware-middleware.mdx b/docs/python-sdk/fastmcp-server-middleware-middleware.mdx
index 428c7e260..4d23d9364 100644
--- a/docs/python-sdk/fastmcp-server-middleware-middleware.mdx
+++ b/docs/python-sdk/fastmcp-server-middleware-middleware.mdx
@@ -66,7 +66,7 @@ on_notification(self, context: MiddlewareContext[mt.Notification[Any, Any]], cal
#### `on_initialize`
```python
-on_initialize(self, context: MiddlewareContext[mt.InitializeRequestParams], call_next: CallNext[mt.InitializeRequestParams, None]) -> None
+on_initialize(self, context: MiddlewareContext[mt.InitializeRequest], call_next: CallNext[mt.InitializeRequest, None]) -> None
```
#### `on_call_tool`
diff --git a/docs/python-sdk/fastmcp-server-openapi.mdx b/docs/python-sdk/fastmcp-server-openapi.mdx
index bc1a1f730..536a4489f 100644
--- a/docs/python-sdk/fastmcp-server-openapi.mdx
+++ b/docs/python-sdk/fastmcp-server-openapi.mdx
@@ -55,7 +55,7 @@ Resource implementation for OpenAPI endpoints.
**Methods:**
-#### `read`
+#### `read`
```python
read(self) -> str | bytes
@@ -64,7 +64,7 @@ read(self) -> str | bytes
Fetch the resource data by making an HTTP request.
-### `OpenAPIResourceTemplate`
+### `OpenAPIResourceTemplate`
Resource template implementation for OpenAPI endpoints.
@@ -72,7 +72,7 @@ Resource template implementation for OpenAPI endpoints.
**Methods:**
-#### `create_resource`
+#### `create_resource`
```python
create_resource(self, uri: str, params: dict[str, Any], context: Context | None = None) -> Resource
@@ -81,7 +81,7 @@ create_resource(self, uri: str, params: dict[str, Any], context: Context | None
Create a resource with the given parameters.
-### `FastMCPOpenAPI`
+### `FastMCPOpenAPI`
FastMCP server implementation that creates components from an OpenAPI schema.
diff --git a/docs/python-sdk/fastmcp-server-proxy.mdx b/docs/python-sdk/fastmcp-server-proxy.mdx
index 3be5ddc97..a64acca64 100644
--- a/docs/python-sdk/fastmcp-server-proxy.mdx
+++ b/docs/python-sdk/fastmcp-server-proxy.mdx
@@ -7,7 +7,7 @@ sidebarTitle: proxy
## Functions
-### `default_proxy_roots_handler`
+### `default_proxy_roots_handler`
```python
default_proxy_roots_handler(context: RequestContext[ClientSession, LifespanContextT]) -> RootsList
@@ -113,7 +113,7 @@ read_resource(self, uri: AnyUrl | str) -> str | bytes
Reads a resource, trying local/mounted first, then proxy if not found.
-### `ProxyPromptManager`
+### `ProxyPromptManager`
A PromptManager that sources its prompts from a remote client in addition to local and mounted prompts.
@@ -121,7 +121,7 @@ A PromptManager that sources its prompts from a remote client in addition to loc
**Methods:**
-#### `get_prompts`
+#### `get_prompts`
```python
get_prompts(self) -> dict[str, Prompt]
@@ -130,7 +130,7 @@ get_prompts(self) -> dict[str, Prompt]
Gets the unfiltered prompt inventory including local, mounted, and proxy prompts.
-#### `list_prompts`
+#### `list_prompts`
```python
list_prompts(self) -> list[Prompt]
@@ -139,7 +139,7 @@ list_prompts(self) -> list[Prompt]
Gets the filtered list of prompts including local, mounted, and proxy prompts.
-#### `render_prompt`
+#### `render_prompt`
```python
render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> GetPromptResult
@@ -148,7 +148,7 @@ render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> GetPr
Renders a prompt, trying local/mounted first, then proxy if not found.
-### `ProxyTool`
+### `ProxyTool`
A Tool that represents and executes a tool on a remote server.
@@ -156,7 +156,7 @@ A Tool that represents and executes a tool on a remote server.
**Methods:**
-#### `from_mcp_tool`
+#### `from_mcp_tool`
```python
from_mcp_tool(cls, client: Client, mcp_tool: mcp.types.Tool) -> ProxyTool
@@ -165,7 +165,7 @@ from_mcp_tool(cls, client: Client, mcp_tool: mcp.types.Tool) -> ProxyTool
Factory method to create a ProxyTool from a raw MCP tool schema.
-#### `run`
+#### `run`
```python
run(self, arguments: dict[str, Any], context: Context | None = None) -> ToolResult
@@ -174,7 +174,7 @@ run(self, arguments: dict[str, Any], context: Context | None = None) -> ToolResu
Executes the tool by making a call through the client.
-### `ProxyResource`
+### `ProxyResource`
A Resource that represents and reads a resource from a remote server.
@@ -182,7 +182,7 @@ A Resource that represents and reads a resource from a remote server.
**Methods:**
-#### `from_mcp_resource`
+#### `from_mcp_resource`
```python
from_mcp_resource(cls, client: Client, mcp_resource: mcp.types.Resource) -> ProxyResource
@@ -191,7 +191,7 @@ from_mcp_resource(cls, client: Client, mcp_resource: mcp.types.Resource) -> Prox
Factory method to create a ProxyResource from a raw MCP resource schema.
-#### `read`
+#### `read`
```python
read(self) -> str | bytes
@@ -200,7 +200,7 @@ read(self) -> str | bytes
Read the resource content from the remote server.
-### `ProxyTemplate`
+### `ProxyTemplate`
A ResourceTemplate that represents and creates resources from a remote server template.
@@ -208,7 +208,7 @@ A ResourceTemplate that represents and creates resources from a remote server te
**Methods:**
-#### `from_mcp_template`
+#### `from_mcp_template`
```python
from_mcp_template(cls, client: Client, mcp_template: mcp.types.ResourceTemplate) -> ProxyTemplate
@@ -217,7 +217,7 @@ from_mcp_template(cls, client: Client, mcp_template: mcp.types.ResourceTemplate)
Factory method to create a ProxyTemplate from a raw MCP template schema.
-#### `create_resource`
+#### `create_resource`
```python
create_resource(self, uri: str, params: dict[str, Any], context: Context | None = None) -> ProxyResource
@@ -226,7 +226,7 @@ create_resource(self, uri: str, params: dict[str, Any], context: Context | None
Create a resource from the template by calling the remote server.
-### `ProxyPrompt`
+### `ProxyPrompt`
A Prompt that represents and renders a prompt from a remote server.
@@ -234,7 +234,7 @@ A Prompt that represents and renders a prompt from a remote server.
**Methods:**
-#### `from_mcp_prompt`
+#### `from_mcp_prompt`
```python
from_mcp_prompt(cls, client: Client, mcp_prompt: mcp.types.Prompt) -> ProxyPrompt
@@ -243,7 +243,7 @@ from_mcp_prompt(cls, client: Client, mcp_prompt: mcp.types.Prompt) -> ProxyPromp
Factory method to create a ProxyPrompt from a raw MCP prompt schema.
-#### `render`
+#### `render`
```python
render(self, arguments: dict[str, Any]) -> list[PromptMessage]
@@ -252,14 +252,14 @@ render(self, arguments: dict[str, Any]) -> list[PromptMessage]
Render the prompt by making a call through the client.
-### `FastMCPProxy`
+### `FastMCPProxy`
A FastMCP server that acts as a proxy to a remote MCP-compliant server.
It uses specialized managers that fulfill requests via a client factory.
-### `ProxyClient`
+### `ProxyClient`
A proxy client that forwards advanced interactions between a remote MCP server and the proxy's connected clients.
@@ -268,7 +268,7 @@ Supports forwarding roots, sampling, elicitation, logging, and progress.
**Methods:**
-#### `default_sampling_handler`
+#### `default_sampling_handler`
```python
default_sampling_handler(cls, messages: list[mcp.types.SamplingMessage], params: mcp.types.CreateMessageRequestParams, context: RequestContext[ClientSession, LifespanContextT]) -> mcp.types.CreateMessageResult
@@ -277,7 +277,7 @@ default_sampling_handler(cls, messages: list[mcp.types.SamplingMessage], params:
A handler that forwards the sampling request from the remote server to the proxy's connected clients and relays the response back to the remote server.
-#### `default_elicitation_handler`
+#### `default_elicitation_handler`
```python
default_elicitation_handler(cls, message: str, response_type: type, params: mcp.types.ElicitRequestParams, context: RequestContext[ClientSession, LifespanContextT]) -> ElicitResult
@@ -286,7 +286,7 @@ default_elicitation_handler(cls, message: str, response_type: type, params: mcp.
A handler that forwards the elicitation request from the remote server to the proxy's connected clients and relays the response back to the remote server.
-#### `default_log_handler`
+#### `default_log_handler`
```python
default_log_handler(cls, message: LogMessage) -> None
@@ -295,7 +295,7 @@ default_log_handler(cls, message: LogMessage) -> None
A handler that forwards the log notification from the remote server to the proxy's connected clients.
-#### `default_progress_handler`
+#### `default_progress_handler`
```python
default_progress_handler(cls, progress: float, total: float | None, message: str | None) -> None
@@ -304,7 +304,7 @@ default_progress_handler(cls, progress: float, total: float | None, message: str
A handler that forwards the progress notification from the remote server to the proxy's connected clients.
-### `StatefulProxyClient`
+### `StatefulProxyClient`
A proxy client that provides a stateful client factory for the proxy server.
@@ -318,7 +318,7 @@ Note that it is essential to ensure that the proxy server itself is also statefu
**Methods:**
-#### `clear`
+#### `clear`
```python
clear(self)
@@ -327,7 +327,7 @@ clear(self)
Clear all cached clients and force disconnect them.
-#### `new_stateful`
+#### `new_stateful`
```python
new_stateful(self) -> Client[ClientTransportT]
diff --git a/docs/python-sdk/fastmcp-server-server.mdx b/docs/python-sdk/fastmcp-server-server.mdx
index cb877c355..1e12460b2 100644
--- a/docs/python-sdk/fastmcp-server-server.mdx
+++ b/docs/python-sdk/fastmcp-server-server.mdx
@@ -10,7 +10,7 @@ FastMCP - A more ergonomic interface for MCP servers.
## Functions
-### `default_lifespan`
+### `default_lifespan`
```python
default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any]
@@ -26,7 +26,7 @@ Default lifespan context manager that does nothing.
- An empty dictionary as the lifespan result.
-### `add_resource_prefix`
+### `add_resource_prefix`
```python
add_resource_prefix(uri: str, prefix: str, prefix_format: Literal['protocol', 'path'] | None = None) -> str
@@ -64,7 +64,7 @@ add_resource_prefix("resource:///absolute/path", "prefix")
- `ValueError`: If the URI doesn't match the expected protocol\://path format
-### `remove_resource_prefix`
+### `remove_resource_prefix`
```python
remove_resource_prefix(uri: str, prefix: str, prefix_format: Literal['protocol', 'path'] | None = None) -> str
@@ -103,7 +103,7 @@ remove_resource_prefix("resource://prefix//absolute/path", "prefix")
- `ValueError`: If the URI doesn't match the expected protocol\://path format
-### `has_resource_prefix`
+### `has_resource_prefix`
```python
has_resource_prefix(uri: str, prefix: str, prefix_format: Literal['protocol', 'path'] | None = None) -> bool
@@ -143,53 +143,53 @@ False
## Classes
-### `FastMCP`
+### `FastMCP`
**Methods:**
-#### `settings`
+#### `settings`
```python
settings(self) -> Settings
```
-#### `name`
+#### `name`
```python
name(self) -> str
```
-#### `instructions`
+#### `instructions`
```python
instructions(self) -> str | None
```
-#### `instructions`
+#### `instructions`
```python
instructions(self, value: str | None) -> None
```
-#### `version`
+#### `version`
```python
version(self) -> str | None
```
-#### `website_url`
+#### `website_url`
```python
website_url(self) -> str | None
```
-#### `icons`
+#### `icons`
```python
icons(self) -> list[mcp.types.Icon]
```
-#### `run_async`
+#### `run_async`
```python
run_async(self, transport: Transport | None = None, show_banner: bool = True, **transport_kwargs: Any) -> None
@@ -201,7 +201,7 @@ Run the FastMCP server asynchronously.
- `transport`: Transport protocol to use ("stdio", "sse", or "streamable-http")
-#### `run`
+#### `run`
```python
run(self, transport: Transport | None = None, show_banner: bool = True, **transport_kwargs: Any) -> None
@@ -213,13 +213,13 @@ Run the FastMCP server. Note this is a synchronous function.
- `transport`: Transport protocol to use ("stdio", "sse", or "streamable-http")
-#### `add_middleware`
+#### `add_middleware`
```python
add_middleware(self, middleware: Middleware) -> None
```
-#### `get_tools`
+#### `get_tools`
```python
get_tools(self) -> dict[str, Tool]
@@ -228,13 +228,13 @@ get_tools(self) -> dict[str, Tool]
Get all tools (unfiltered), including mounted servers, indexed by key.
-#### `get_tool`
+#### `get_tool`
```python
get_tool(self, key: str) -> Tool
```
-#### `get_resources`
+#### `get_resources`
```python
get_resources(self) -> dict[str, Resource]
@@ -243,13 +243,13 @@ get_resources(self) -> dict[str, Resource]
Get all resources (unfiltered), including mounted servers, indexed by key.
-#### `get_resource`
+#### `get_resource`
```python
get_resource(self, key: str) -> Resource
```
-#### `get_resource_templates`
+#### `get_resource_templates`
```python
get_resource_templates(self) -> dict[str, ResourceTemplate]
@@ -258,7 +258,7 @@ get_resource_templates(self) -> dict[str, ResourceTemplate]
Get all resource templates (unfiltered), including mounted servers, indexed by key.
-#### `get_resource_template`
+#### `get_resource_template`
```python
get_resource_template(self, key: str) -> ResourceTemplate
@@ -267,7 +267,7 @@ get_resource_template(self, key: str) -> ResourceTemplate
Get a registered resource template by key.
-#### `get_prompts`
+#### `get_prompts`
```python
get_prompts(self) -> dict[str, Prompt]
@@ -276,13 +276,13 @@ get_prompts(self) -> dict[str, Prompt]
Get all prompts (unfiltered), including mounted servers, indexed by key.
-#### `get_prompt`
+#### `get_prompt`
```python
get_prompt(self, key: str) -> Prompt
```
-#### `custom_route`
+#### `custom_route`
```python
custom_route(self, path: str, methods: list[str], name: str | None = None, include_in_schema: bool = True) -> Callable[[Callable[[Request], Awaitable[Response]]], Callable[[Request], Awaitable[Response]]]
@@ -303,7 +303,7 @@ Starlette's reverse URL lookup feature)
- `include_in_schema`: Whether to include in OpenAPI schema, defaults to True
-#### `add_tool`
+#### `add_tool`
```python
add_tool(self, tool: Tool) -> Tool
@@ -321,7 +321,7 @@ with the Context type annotation. See the @tool decorator for examples.
- The tool instance that was added to the server.
-#### `remove_tool`
+#### `remove_tool`
```python
remove_tool(self, name: str) -> None
@@ -336,7 +336,7 @@ Remove a tool from the server.
- `NotFoundError`: If the tool is not found
-#### `add_tool_transformation`
+#### `add_tool_transformation`
```python
add_tool_transformation(self, tool_name: str, transformation: ToolTransformConfig) -> None
@@ -345,7 +345,7 @@ add_tool_transformation(self, tool_name: str, transformation: ToolTransformConfi
Add a tool transformation.
-#### `remove_tool_transformation`
+#### `remove_tool_transformation`
```python
remove_tool_transformation(self, tool_name: str) -> None
@@ -354,19 +354,19 @@ remove_tool_transformation(self, tool_name: str) -> None
Remove a tool transformation.
-#### `tool`
+#### `tool`
```python
tool(self, name_or_fn: AnyFunction) -> FunctionTool
```
-#### `tool`
+#### `tool`
```python
tool(self, name_or_fn: str | None = None) -> Callable[[AnyFunction], FunctionTool]
```
-#### `tool`
+#### `tool`
```python
tool(self, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionTool] | FunctionTool
@@ -422,7 +422,7 @@ server.tool(my_function, name="custom_name")
```
-#### `add_resource`
+#### `add_resource`
```python
add_resource(self, resource: Resource) -> Resource
@@ -437,7 +437,7 @@ Add a resource to the server.
- The resource instance that was added to the server.
-#### `add_template`
+#### `add_template`
```python
add_template(self, template: ResourceTemplate) -> ResourceTemplate
@@ -452,7 +452,7 @@ Add a resource template to the server.
- The template instance that was added to the server.
-#### `add_resource_fn`
+#### `add_resource_fn`
```python
add_resource_fn(self, fn: AnyFunction, uri: str, name: str | None = None, description: str | None = None, mime_type: str | None = None, tags: set[str] | None = None) -> None
@@ -472,7 +472,7 @@ has parameters, it will be registered as a template resource.
- `tags`: Optional set of tags for categorizing the resource
-#### `resource`
+#### `resource`
```python
resource(self, uri: str) -> Callable[[AnyFunction], Resource | ResourceTemplate]
@@ -532,7 +532,7 @@ async def get_weather(city: str) -> str:
```
-#### `add_prompt`
+#### `add_prompt`
```python
add_prompt(self, prompt: Prompt) -> Prompt
@@ -547,19 +547,19 @@ Add a prompt to the server.
- The prompt instance that was added to the server.
-#### `prompt`
+#### `prompt`
```python
prompt(self, name_or_fn: AnyFunction) -> FunctionPrompt
```
-#### `prompt`
+#### `prompt`
```python
prompt(self, name_or_fn: str | None = None) -> Callable[[AnyFunction], FunctionPrompt]
```
-#### `prompt`
+#### `prompt`
```python
prompt(self, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt
@@ -637,7 +637,7 @@ Decorator to register a prompt.
```
-#### `run_stdio_async`
+#### `run_stdio_async`
```python
run_stdio_async(self, show_banner: bool = True, log_level: str | None = None) -> None
@@ -650,7 +650,7 @@ Run the server using stdio transport.
- `log_level`: Log level for the server
-#### `run_http_async`
+#### `run_http_async`
```python
run_http_async(self, show_banner: bool = True, transport: Literal['http', 'streamable-http', 'sse'] = 'http', host: str | None = None, port: int | None = None, log_level: str | None = None, path: str | None = None, uvicorn_config: dict[str, Any] | None = None, middleware: list[ASGIMiddleware] | None = None, json_response: bool | None = None, stateless_http: bool | None = None) -> None
@@ -670,7 +670,7 @@ Run the server using HTTP transport.
- `stateless_http`: Whether to use stateless HTTP (defaults to settings.stateless_http)
-#### `run_sse_async`
+#### `run_sse_async`
```python
run_sse_async(self, host: str | None = None, port: int | None = None, log_level: str | None = None, path: str | None = None, uvicorn_config: dict[str, Any] | None = None) -> None
@@ -679,7 +679,7 @@ run_sse_async(self, host: str | None = None, port: int | None = None, log_level:
Run the server using SSE transport.
-#### `sse_app`
+#### `sse_app`
```python
sse_app(self, path: str | None = None, message_path: str | None = None, middleware: list[ASGIMiddleware] | None = None) -> StarletteWithLifespan
@@ -693,7 +693,7 @@ Create a Starlette app for the SSE server.
- `middleware`: A list of middleware to apply to the app
-#### `streamable_http_app`
+#### `streamable_http_app`
```python
streamable_http_app(self, path: str | None = None, middleware: list[ASGIMiddleware] | None = None) -> StarletteWithLifespan
@@ -706,7 +706,7 @@ Create a Starlette app for the StreamableHTTP server.
- `middleware`: A list of middleware to apply to the app
-#### `http_app`
+#### `http_app`
```python
http_app(self, path: str | None = None, middleware: list[ASGIMiddleware] | None = None, json_response: bool | None = None, stateless_http: bool | None = None, transport: Literal['http', 'streamable-http', 'sse'] = 'http') -> StarletteWithLifespan
@@ -723,7 +723,7 @@ Create a Starlette app using the specified HTTP transport.
- A Starlette application configured with the specified transport
-#### `mount`
+#### `mount`
```python
mount(self, server: FastMCP[LifespanResultT], prefix: str | None = None, as_proxy: bool | None = None) -> None
@@ -777,7 +777,7 @@ automatically determined based on whether the server has a custom lifespan
- `prompt_separator`: Deprecated. Separator character for prompt names.
-#### `import_server`
+#### `import_server`
```python
import_server(self, server: FastMCP[LifespanResultT], prefix: str | None = None, tool_separator: str | None = None, resource_separator: str | None = None, prompt_separator: str | None = None) -> None
@@ -818,7 +818,7 @@ applied using the protocol\://prefix/path format
- `prompt_separator`: Deprecated. Separator for prompt names.
-#### `from_openapi`
+#### `from_openapi`
```python
from_openapi(cls, openapi_spec: dict[str, Any], client: httpx.AsyncClient, route_maps: list[RouteMap] | list[RouteMapNew] | None = None, route_map_fn: OpenAPIRouteMapFn | OpenAPIRouteMapFnNew | None = None, mcp_component_fn: OpenAPIComponentFn | OpenAPIComponentFnNew | None = None, mcp_names: dict[str, str] | None = None, tags: set[str] | None = None, **settings: Any) -> FastMCPOpenAPI | FastMCPOpenAPINew
@@ -827,7 +827,7 @@ from_openapi(cls, openapi_spec: dict[str, Any], client: httpx.AsyncClient, route
Create a FastMCP server from an OpenAPI specification.
-#### `from_fastapi`
+#### `from_fastapi`
```python
from_fastapi(cls, app: Any, name: str | None = None, route_maps: list[RouteMap] | list[RouteMapNew] | None = None, route_map_fn: OpenAPIRouteMapFn | OpenAPIRouteMapFnNew | None = None, mcp_component_fn: OpenAPIComponentFn | OpenAPIComponentFnNew | None = None, mcp_names: dict[str, str] | None = None, httpx_client_kwargs: dict[str, Any] | None = None, tags: set[str] | None = None, **settings: Any) -> FastMCPOpenAPI | FastMCPOpenAPINew
@@ -836,10 +836,10 @@ from_fastapi(cls, app: Any, name: str | None = None, route_maps: list[RouteMap]
Create a FastMCP server from a FastAPI application.
-#### `as_proxy`
+#### `as_proxy`
```python
-as_proxy(cls, backend: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy
+as_proxy(cls, backend: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy
```
Create a FastMCP proxy server for the given backend.
@@ -850,7 +850,7 @@ instance or any value accepted as the `transport` argument of
`fastmcp.client.Client` constructor.
-#### `from_client`
+#### `from_client`
```python
from_client(cls, client: Client[ClientTransportT], **settings: Any) -> FastMCPProxy
@@ -859,10 +859,10 @@ from_client(cls, client: Client[ClientTransportT], **settings: Any) -> FastMCPPr
Create a FastMCP proxy server from a FastMCP client.
-#### `generate_name`
+#### `generate_name`
```python
generate_name(cls, name: str | None = None) -> str
```
-### `MountedServer`
+### `MountedServer`
diff --git a/docs/python-sdk/fastmcp-tools-tool.mdx b/docs/python-sdk/fastmcp-tools-tool.mdx
index 791da686c..b55865525 100644
--- a/docs/python-sdk/fastmcp-tools-tool.mdx
+++ b/docs/python-sdk/fastmcp-tools-tool.mdx
@@ -19,13 +19,13 @@ default_serializer(data: Any) -> str
**Methods:**
-#### `to_mcp_result`
+#### `to_mcp_result`
```python
-to_mcp_result(self) -> list[ContentBlock] | tuple[list[ContentBlock], dict[str, Any]]
+to_mcp_result(self) -> list[ContentBlock] | tuple[list[ContentBlock], dict[str, Any]] | CallToolResult
```
-### `Tool`
+### `Tool`
Internal tool registration info.
@@ -33,19 +33,19 @@ Internal tool registration info.
**Methods:**
-#### `enable`
+#### `enable`
```python
enable(self) -> None
```
-#### `disable`
+#### `disable`
```python
disable(self) -> None
```
-#### `to_mcp_tool`
+#### `to_mcp_tool`
```python
to_mcp_tool(self, **overrides: Any) -> MCPTool
@@ -54,16 +54,16 @@ to_mcp_tool(self, **overrides: Any) -> MCPTool
Convert the FastMCP tool to an MCP tool.
-#### `from_function`
+#### `from_function`
```python
-from_function(fn: Callable[..., Any], name: str | None = None, title: str | None = None, description: str | None = None, icons: list[Icon] | None = None, tags: set[str] | None = None, annotations: ToolAnnotations | None = None, exclude_args: list[str] | None = None, output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet, serializer: ToolResultSerializerType | None = None, meta: dict[str, Any] | None = None, enabled: bool | None = None) -> FunctionTool
+from_function(fn: Callable[..., Any], name: str | None = None, title: str | None = None, description: str | None = None, icons: list[Icon] | None = None, tags: set[str] | None = None, annotations: ToolAnnotations | None = None, exclude_args: list[str] | None = None, output_schema: dict[str, Any] | Literal[False] | NotSetT | None = NotSet, serializer: ToolResultSerializerType | None = None, meta: dict[str, Any] | None = None, enabled: bool | None = None) -> FunctionTool
```
Create a Tool from a function.
-#### `run`
+#### `run`
```python
run(self, arguments: dict[str, Any]) -> ToolResult
@@ -78,26 +78,26 @@ implemented by subclasses.
(list of ContentBlocks, dict of structured output).
-#### `from_tool`
+#### `from_tool`
```python
from_tool(cls, tool: Tool) -> TransformedTool
```
-### `FunctionTool`
+### `FunctionTool`
**Methods:**
-#### `from_function`
+#### `from_function`
```python
-from_function(cls, fn: Callable[..., Any], name: str | None = None, title: str | None = None, description: str | None = None, icons: list[Icon] | None = None, tags: set[str] | None = None, annotations: ToolAnnotations | None = None, exclude_args: list[str] | None = None, output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet, serializer: ToolResultSerializerType | None = None, meta: dict[str, Any] | None = None, enabled: bool | None = None) -> FunctionTool
+from_function(cls, fn: Callable[..., Any], name: str | None = None, title: str | None = None, description: str | None = None, icons: list[Icon] | None = None, tags: set[str] | None = None, annotations: ToolAnnotations | None = None, exclude_args: list[str] | None = None, output_schema: dict[str, Any] | Literal[False] | NotSetT | None = NotSet, serializer: ToolResultSerializerType | None = None, meta: dict[str, Any] | None = None, enabled: bool | None = None) -> FunctionTool
```
Create a Tool from a function.
-#### `run`
+#### `run`
```python
run(self, arguments: dict[str, Any]) -> ToolResult
@@ -106,11 +106,11 @@ run(self, arguments: dict[str, Any]) -> ToolResult
Run the tool with arguments.
-### `ParsedFunction`
+### `ParsedFunction`
**Methods:**
-#### `from_function`
+#### `from_function`
```python
from_function(cls, fn: Callable[..., Any], exclude_args: list[str] | None = None, validate: bool = True, wrap_non_object_output_schema: bool = True) -> ParsedFunction
diff --git a/docs/python-sdk/fastmcp-tools-tool_transform.mdx b/docs/python-sdk/fastmcp-tools-tool_transform.mdx
index e3d856876..c9c416543 100644
--- a/docs/python-sdk/fastmcp-tools-tool_transform.mdx
+++ b/docs/python-sdk/fastmcp-tools-tool_transform.mdx
@@ -193,7 +193,7 @@ functions.
#### `from_tool`
```python
-from_tool(cls, tool: Tool, name: str | None = None, title: str | None | NotSetT = NotSet, description: str | None | NotSetT = NotSet, tags: set[str] | None = None, transform_fn: Callable[..., Any] | None = None, transform_args: dict[str, ArgTransform] | None = None, annotations: ToolAnnotations | None | NotSetT = NotSet, output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet, serializer: Callable[[Any], str] | None | NotSetT = NotSet, meta: dict[str, Any] | None | NotSetT = NotSet, enabled: bool | None = None) -> TransformedTool
+from_tool(cls, tool: Tool, name: str | None = None, title: str | NotSetT | None = NotSet, description: str | NotSetT | None = NotSet, tags: set[str] | None = None, transform_fn: Callable[..., Any] | None = None, transform_args: dict[str, ArgTransform] | None = None, annotations: ToolAnnotations | NotSetT | None = NotSet, output_schema: dict[str, Any] | Literal[False] | NotSetT | None = NotSet, serializer: Callable[[Any], str] | NotSetT | None = NotSet, meta: dict[str, Any] | NotSetT | None = NotSet, enabled: bool | None = None) -> TransformedTool
```
Create a transformed tool from a parent tool.
diff --git a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-base.mdx b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-base.mdx
index 3a97b5d9e..df7fc00f4 100644
--- a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-base.mdx
+++ b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-base.mdx
@@ -30,7 +30,7 @@ Build the full command with environment setup.
- Full command ready for subprocess execution
-#### `prepare`
+#### `prepare`
```python
prepare(self, output_dir: Path | None = None) -> None
diff --git a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-base.mdx b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-base.mdx
index 0a2aa84ac..5541fcb6f 100644
--- a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-base.mdx
+++ b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-base.mdx
@@ -28,7 +28,7 @@ this method performs that preparation. For sources that don't
need preparation (e.g., local files), this is a no-op.
-#### `load_server`
+#### `load_server`
```python
load_server(self) -> Any
diff --git a/docs/python-sdk/fastmcp-utilities-tests.mdx b/docs/python-sdk/fastmcp-utilities-tests.mdx
index 37784a90d..a33f1122b 100644
--- a/docs/python-sdk/fastmcp-utilities-tests.mdx
+++ b/docs/python-sdk/fastmcp-utilities-tests.mdx
@@ -64,7 +64,7 @@ sleeps, and cleanup issues.
- `host`: Host to bind to (default\: "127.0.0.1")
-### `caplog_for_fastmcp`
+### `caplog_for_fastmcp`
```python
caplog_for_fastmcp(caplog)
@@ -76,7 +76,7 @@ Context manager to capture logs from FastMCP loggers even when propagation is di
## Classes
-### `HeadlessOAuth`
+### `HeadlessOAuth`
OAuth provider that bypasses browser interaction for testing.
@@ -87,7 +87,7 @@ instead of opening a browser and running a callback server. Useful for automated
**Methods:**
-#### `redirect_handler`
+#### `redirect_handler`
```python
redirect_handler(self, authorization_url: str) -> None
@@ -96,7 +96,7 @@ redirect_handler(self, authorization_url: str) -> None
Make HTTP request to authorization URL and store response for callback handler.
-#### `callback_handler`
+#### `callback_handler`
```python
callback_handler(self) -> tuple[str, str | None]
diff --git a/docs/python-sdk/fastmcp-utilities-types.mdx b/docs/python-sdk/fastmcp-utilities-types.mdx
index 8b6cf44ca..23f694228 100644
--- a/docs/python-sdk/fastmcp-utilities-types.mdx
+++ b/docs/python-sdk/fastmcp-utilities-types.mdx
@@ -64,7 +64,7 @@ Find the name of the kwarg that is of type kwarg_type.
Includes union types that contain the kwarg_type, as well as Annotated types.
-### `replace_type`
+### `replace_type`
```python
replace_type(type_, type_map: dict[type, type])
@@ -107,7 +107,7 @@ Helper class for returning images from tools.
**Methods:**
-#### `to_image_content`
+#### `to_image_content`
```python
to_image_content(self, mime_type: str | None = None, annotations: Annotations | None = None) -> mcp.types.ImageContent
@@ -116,7 +116,16 @@ to_image_content(self, mime_type: str | None = None, annotations: Annotations |
Convert to MCP ImageContent.
-### `Audio`
+#### `to_data_uri`
+
+```python
+to_data_uri(self, mime_type: str | None = None) -> str
+```
+
+Get image as a data URI.
+
+
+### `Audio`
Helper class for returning audio from tools.
@@ -124,13 +133,13 @@ Helper class for returning audio from tools.
**Methods:**
-#### `to_audio_content`
+#### `to_audio_content`
```python
to_audio_content(self, mime_type: str | None = None, annotations: Annotations | None = None) -> mcp.types.AudioContent
```
-### `File`
+### `File`
Helper class for returning file data from tools.
@@ -138,10 +147,10 @@ Helper class for returning file data from tools.
**Methods:**
-#### `to_resource_content`
+#### `to_resource_content`
```python
to_resource_content(self, mime_type: str | None = None, annotations: Annotations | None = None) -> mcp.types.EmbeddedResource
```
-### `ContextSamplingFallbackProtocol`
+### `ContextSamplingFallbackProtocol`
diff --git a/docs/servers/context.mdx b/docs/servers/context.mdx
index c33f1812c..1fb88ffad 100644
--- a/docs/servers/context.mdx
+++ b/docs/servers/context.mdx
@@ -313,6 +313,35 @@ async def request_info(ctx: Context) -> dict:
- **`ctx.client_id -> str | None`**: Get the ID of the client making the request, if provided during initialization
- **`ctx.session_id -> str | None`**: Get the MCP session ID for session-based data sharing (HTTP transports only)
+#### Client Metadata
+
+
+
+Clients can send contextual information with their requests using the `meta` parameter. This metadata is accessible through `ctx.request_context.meta` and is available for all MCP operations (tools, resources, prompts).
+
+The `meta` field is `None` when clients don't provide metadata. When provided, metadata is accessible via attribute access (e.g., `meta.user_id`) rather than dictionary access. The structure of metadata is determined by the client making the request.
+
+```python
+@mcp.tool
+def send_email(to: str, subject: str, body: str, ctx: Context) -> str:
+ """Send an email, logging metadata about the request."""
+
+ # Access client-provided metadata
+ meta = ctx.request_context.meta
+
+ if meta:
+ # Meta is accessed as an object with attribute access
+ user_id = meta.user_id if hasattr(meta, 'user_id') else None
+ trace_id = meta.trace_id if hasattr(meta, 'trace_id') else None
+
+ # Use metadata for logging, observability, etc.
+ if trace_id:
+ log_with_trace(f"Sending email for user {user_id}", trace_id)
+
+ # Send the email...
+ return f"Email sent to {to}"
+```
+
The MCP request is part of the low-level MCP SDK and intended for advanced use cases. Most users will not need to use it directly.
diff --git a/docs/servers/tools.mdx b/docs/servers/tools.mdx
index 8b2279ccf..e326258d2 100644
--- a/docs/servers/tools.mdx
+++ b/docs/servers/tools.mdx
@@ -453,62 +453,89 @@ The 6/18/2025 MCP spec update [introduced](https://modelcontextprotocol.io/speci
This automatic behavior enables clients to receive machine-readable data alongside human-readable content without requiring explicit output schemas for object-like returns.
-#### Object-like Results (Automatic Structured Content)
+#### Dictionaries and Objects
+
+When your tool returns a dictionary, dataclass, or Pydantic model, FastMCP automatically creates structured content from it. The structured content contains the actual object data, making it easy for clients to deserialize back to native objects.
-```python Dict Return (No Schema Needed)
+```python Tool Definition
@mcp.tool
def get_user_data(user_id: str) -> dict:
- """Get user data without type annotation."""
+ """Get user data."""
return {"name": "Alice", "age": 30, "active": True}
```
-```json Traditional Content
-"{\n \"name\": \"Alice\",\n \"age\": 30,\n \"active\": true\n}"
-```
-
-```json Structured Content (Automatic)
+```json MCP Result
{
- "name": "Alice",
- "age": 30,
- "active": true
+ "content": [
+ {
+ "type": "text",
+ "text": "{\n \"name\": \"Alice\",\n \"age\": 30,\n \"active\": true\n}"
+ }
+ ],
+ "structuredContent": {
+ "name": "Alice",
+ "age": 30,
+ "active": true
+ }
}
```
-#### Non-object Results (Schema Required)
+#### Primitives and Collections
+
+When your tool returns a primitive type (int, str, bool) or a collection (list, set), FastMCP needs a return type annotation to generate structured content. The annotation tells FastMCP how to validate and serialize the result.
+
+Without a type annotation, the tool only produces `content`:
-```python Integer Return (No Schema)
-@mcp.tool
+```python Tool Definition
+@mcp.tool
def calculate_sum(a: int, b: int):
"""Calculate sum without return annotation."""
return a + b # Returns 8
```
-```json Traditional Content Only
-"8"
-```
-
-```python Integer Return (With Schema)
-@mcp.tool
-def calculate_sum(a: int, b: int) -> int:
- """Calculate sum with return annotation."""
- return a + b # Returns 8
-```
-
-```json Traditional Content
-"8"
-```
-
-```json Structured Content (From Schema)
+```json MCP Result
{
- "result": 8
+ "content": [
+ {
+ "type": "text",
+ "text": "8"
+ }
+ ]
}
```
-#### Complex Type Example
+When you add a return annotation, such as `-> int`, FastMCP generates `structuredContent` by wrapping the primitive value in a `{"result": ...}` object, since JSON schemas require object-type roots for structured output:
+
+
+```python Tool Definition
+@mcp.tool
+def calculate_sum(a: int, b: int) -> int:
+ """Calculate sum with return annotation."""
+ return a + b # Returns 8
+```
+
+```json MCP Result
+{
+ "content": [
+ {
+ "type": "text",
+ "text": "8"
+ }
+ ],
+ "structuredContent": {
+ "result": 8
+ }
+}
+```
+
+
+#### Typed Models
+
+Return type annotations work with any type that can be converted to a JSON schema. Dataclasses and Pydantic models are particularly useful because FastMCP extracts their field definitions to create detailed schemas.
```python Tool Definition
@@ -526,14 +553,18 @@ class Person:
@mcp.tool
def get_user_profile(user_id: str) -> Person:
"""Get a user's profile information."""
- return Person(name="Alice", age=30, email="alice@example.com")
+ return Person(
+ name="Alice",
+ age=30,
+ email="alice@example.com",
+ )
```
```json Generated Output Schema
{
"properties": {
"name": {"title": "Name", "type": "string"},
- "age": {"title": "Age", "type": "integer"},
+ "age": {"title": "Age", "type": "integer"},
"email": {"title": "Email", "type": "string"}
},
"required": ["name", "age", "email"],
@@ -542,15 +573,25 @@ def get_user_profile(user_id: str) -> Person:
}
```
-```json Structured Output
+```json MCP Result
{
- "name": "Alice",
- "age": 30,
- "email": "alice@example.com"
+ "content": [
+ {
+ "type": "text",
+ "text": "{\"name\": \"Alice\", \"age\": 30, \"email\": \"alice@example.com\"}"
+ }
+ ],
+ "structuredContent": {
+ "name": "Alice",
+ "age": 30,
+ "email": "alice@example.com"
+ }
}
```
+The `Person` dataclass becomes an output schema (second tab) that describes the expected format. When executed, clients receive the result (third tab) with both `content` and `structuredContent` fields.
+
### Output Schemas
@@ -614,31 +655,70 @@ Schema generation works for most common types including basic types, collections
- However, you can provide structured output without an output schema (using `ToolResult`)
-### Full Control with ToolResult
+### ToolResult and Metadata
-For complete control over both traditional content and structured output, return a `ToolResult` object:
+For complete control over tool responses, return a `ToolResult` object. This gives you explicit control over all aspects of the tool's output: traditional content, structured data, and metadata.
```python
from fastmcp.tools.tool import ToolResult
+from mcp.types import TextContent
@mcp.tool
def advanced_tool() -> ToolResult:
"""Tool with full control over output."""
return ToolResult(
content=[TextContent(type="text", text="Human-readable summary")],
- structured_content={"data": "value", "count": 42}
+ structured_content={"data": "value", "count": 42},
+ meta={"execution_time_ms": 145}
)
```
-When returning `ToolResult`:
-- You control exactly what content and structured data is sent
-- Output schemas are optional - structured content can be provided without a schema
-- Clients receive both traditional content blocks and structured data
+`ToolResult` accepts three fields:
+
+**`content`** - The traditional MCP content blocks that clients display to users. Can be a string (automatically converted to `TextContent`), a list of MCP content blocks, or any serializable value (converted to JSON string). At least one of `content` or `structured_content` must be provided.
+
+```python
+# Simple string
+ToolResult(content="Hello, world!")
+
+# List of content blocks
+ToolResult(content=[
+ TextContent(type="text", text="Result: 42"),
+ ImageContent(type="image", data="base64...", mimeType="image/png")
+])
+```
+
+**`structured_content`** - A dictionary containing structured data that matches your tool's output schema. This enables clients to programmatically process the results. If you provide `structured_content`, it must be a dictionary or `None`. If only `structured_content` is provided, it will also be used as `content` (converted to JSON string).
+
+```python
+ToolResult(
+ content="Found 3 users",
+ structured_content={"users": [{"name": "Alice"}, {"name": "Bob"}]}
+)
+```
+
+**`meta`**
+
+Runtime metadata about the tool execution. Use this for performance metrics, debugging information, or any client-specific data that doesn't belong in the content or structured output.
+
+```python
+ToolResult(
+ content="Analysis complete",
+ structured_content={"result": "positive"},
+ meta={
+ "execution_time_ms": 145,
+ "model_version": "2.1",
+ "confidence": 0.95
+ }
+)
+```
-If your return type annotation cannot be converted to a JSON schema (e.g., complex custom classes without Pydantic support), the output schema will be omitted but the tool will still function normally with traditional content.
+The `meta` field in `ToolResult` is for runtime metadata about tool execution (e.g., execution time, performance metrics). This is separate from the `meta` parameter in `@mcp.tool(meta={...})`, which provides static metadata about the tool definition itself.
+When returning `ToolResult`, you have full control - FastMCP won't automatically wrap or transform your data. `ToolResult` can be returned with or without an output schema.
+
## Error Handling
diff --git a/examples/tool_result_echo.py b/examples/tool_result_echo.py
new file mode 100644
index 000000000..bd1185f29
--- /dev/null
+++ b/examples/tool_result_echo.py
@@ -0,0 +1,41 @@
+"""
+FastMCP Echo Server with Metadata
+
+Demonstrates how to return metadata alongside content and structured data.
+The meta field can include execution details, versioning, or other information
+that clients may find useful.
+"""
+
+import time
+from dataclasses import dataclass
+
+from fastmcp import FastMCP
+from fastmcp.tools.tool import ToolResult
+
+mcp = FastMCP("Echo Server")
+
+
+@dataclass
+class EchoData:
+ data: str
+ length: int
+
+
+@mcp.tool
+def echo(text: str) -> ToolResult:
+ """Echo text back with metadata about the operation."""
+ start = time.perf_counter()
+
+ result = EchoData(data=text, length=len(text))
+
+ execution_time = (time.perf_counter() - start) * 1000
+
+ return ToolResult(
+ content=f"Echoed: {text}",
+ structured_content=result,
+ meta={
+ "execution_time_ms": round(execution_time, 2),
+ "character_count": len(text),
+ "word_count": len(text.split()),
+ },
+ )
diff --git a/pyproject.toml b/pyproject.toml
index fec290757..e20f60047 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,12 +7,12 @@ dependencies = [
"python-dotenv>=1.1.0",
"exceptiongroup>=1.2.2",
"httpx>=0.28.1",
- "mcp>=1.17.0,<2.0.0",
+ "mcp>=1.19.0,<2.0.0",
"openapi-pydantic>=0.5.1",
"platformdirs>=4.0.0",
"rich>=13.9.4",
- "cyclopts>=3.0.0",
- "authlib>=1.5.2",
+ "cyclopts>=4.0.0",
+ "authlib>=1.6.5",
"pydantic[email]>=2.11.7",
"pyperclip>=1.9.0",
"py-key-value-aio[disk,keyring,memory]>=0.2.8,<0.3.0",
@@ -56,23 +56,23 @@ dev = [
"fastapi>=0.115.12",
"inline-snapshot[dirty-equals]>=0.27.2",
"ipython>=8.12.3",
- "pdbpp>=0.10.3",
- "pre-commit",
- "psutil",
+ "pdbpp>=0.11.7",
+ "psutil>=7.0.0",
"pyinstrument>=5.0.2",
"pyperclip>=1.9.0",
"pytest>=8.3.3",
"pytest-asyncio>=1.2.0",
"pytest-cov>=6.1.1",
"pytest-env>=1.1.5",
- "pytest-flakefinder",
+ "pytest-flakefinder>=1.1.0",
"pytest-httpx>=0.35.0",
"pytest-report>=0.2.1",
"pytest-retry>=1.7.0",
"pytest-timeout>=2.4.0",
"pytest-xdist>=3.6.1",
- "ruff",
- "ty>=0.0.1a19",
+ "ruff>=0.12.8",
+ "ty==0.0.1a25",
+ "prek>=0.2.12",
]
[project.scripts]
@@ -136,12 +136,17 @@ python-version = "3.10"
[tool.ty.rules]
# Rules with too many errors to fix right now (40+ each)
-no-matching-overload = "ignore" # 126 errors
+no-matching-overload = "ignore" # 126 errors
unknown-argument = "ignore" # 61 errors
# Rules with moderate errors that need more investigation
call-non-callable = "ignore" # 7 errors
+# NOTE: ty currently doesn't support type narrowing with isinstance() on unions
+# See: https://github.com/astral-sh/ty/issues/122 and https://github.com/astral-sh/ty/issues/1113
+# Some code uses `# ty: ignore[invalid-argument-type]` for this limitation.
+# TODO: Remove these ignores once ty supports union narrowing
+
[tool.ruff.lint]
fixable = ["ALL"]
ignore = [
diff --git a/src/fastmcp/cli/install/cursor.py b/src/fastmcp/cli/install/cursor.py
index ee0edb2c6..de7fb6037 100644
--- a/src/fastmcp/cli/install/cursor.py
+++ b/src/fastmcp/cli/install/cursor.py
@@ -1,10 +1,12 @@
"""Cursor integration for FastMCP install using Cyclopts."""
import base64
+import os
import subprocess
import sys
from pathlib import Path
from typing import Annotated
+from urllib.parse import quote, urlparse
import cyclopts
from rich import print
@@ -36,8 +38,9 @@ def generate_cursor_deeplink(
config_json = server_config.model_dump_json(exclude_none=True)
config_b64 = base64.urlsafe_b64encode(config_json.encode()).decode()
- # Generate the deeplink URL
- deeplink = f"cursor://anysphere.cursor-deeplink/mcp/install?name={server_name}&config={config_b64}"
+ # Generate the deeplink URL with properly encoded server name
+ encoded_name = quote(server_name, safe="")
+ deeplink = f"cursor://anysphere.cursor-deeplink/mcp/install?name={encoded_name}&config={config_b64}"
return deeplink
@@ -51,17 +54,20 @@ def open_deeplink(deeplink: str) -> bool:
Returns:
True if the command succeeded, False otherwise
"""
+ parsed = urlparse(deeplink)
+ if parsed.scheme != "cursor":
+ logger.warning(f"Invalid deeplink scheme: {parsed.scheme}")
+ return False
+
try:
if sys.platform == "darwin": # macOS
subprocess.run(["open", deeplink], check=True, capture_output=True)
elif sys.platform == "win32": # Windows
- subprocess.run(
- ["cmd", "/c", "start", deeplink], check=True, capture_output=True
- )
+ os.startfile(deeplink)
else: # Linux and others
subprocess.run(["xdg-open", deeplink], check=True, capture_output=True)
return True
- except (subprocess.CalledProcessError, FileNotFoundError):
+ except (subprocess.CalledProcessError, FileNotFoundError, OSError):
return False
diff --git a/src/fastmcp/client/auth/oauth.py b/src/fastmcp/client/auth/oauth.py
index f454febec..349db61b3 100644
--- a/src/fastmcp/client/auth/oauth.py
+++ b/src/fastmcp/client/auth/oauth.py
@@ -12,6 +12,7 @@ from key_value.aio.adapters.pydantic import PydanticAdapter
from key_value.aio.protocols import AsyncKeyValue
from key_value.aio.stores.memory import MemoryStore
from mcp.client.auth import OAuthClientProvider, TokenStorage
+from mcp.shared._httpx_utils import McpHttpClientFactory
from mcp.shared.auth import (
OAuthClientInformationFull,
OAuthClientMetadata,
@@ -147,6 +148,7 @@ class OAuth(OAuthClientProvider):
token_storage: AsyncKeyValue | None = None,
additional_client_metadata: dict[str, Any] | None = None,
callback_port: int | None = None,
+ httpx_client_factory: McpHttpClientFactory | None = None,
):
"""
Initialize OAuth client provider for an MCP server.
@@ -164,6 +166,7 @@ class OAuth(OAuthClientProvider):
server_base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
# Setup OAuth client
+ self.httpx_client_factory = httpx_client_factory or httpx.AsyncClient
self.redirect_port = callback_port or find_available_port()
redirect_uri = f"http://localhost:{self.redirect_port}/callback"
@@ -226,7 +229,7 @@ class OAuth(OAuthClientProvider):
async def redirect_handler(self, authorization_url: str) -> None:
"""Open browser for authorization, with pre-flight check for invalid client."""
# Pre-flight check to detect invalid client_id before opening browser
- async with httpx.AsyncClient() as client:
+ async with self.httpx_client_factory() as client:
response = await client.get(authorization_url, follow_redirects=False)
# Check for client not found error (400 typically means bad client_id)
@@ -297,7 +300,8 @@ class OAuth(OAuthClientProvider):
response = None
while True:
try:
- yielded_request = await gen.asend(response)
+ # First iteration sends None, subsequent iterations send response
+ yielded_request = await gen.asend(response) # ty: ignore[invalid-argument-type]
response = yield yielded_request
except StopAsyncIteration:
break
@@ -306,16 +310,16 @@ class OAuth(OAuthClientProvider):
logger.debug(
"OAuth client not found on server, clearing cache and retrying..."
)
-
# Clear cached state and retry once
self._initialized = False
await self.token_storage_adapter.clear()
+ # Retry with fresh registration
gen = super().async_auth_flow(request)
response = None
while True:
try:
- yielded_request = await gen.asend(response)
+ yielded_request = await gen.asend(response) # ty: ignore[invalid-argument-type]
response = yield yielded_request
except StopAsyncIteration:
break
diff --git a/src/fastmcp/client/client.py b/src/fastmcp/client/client.py
index d24350d38..90bbd32d9 100644
--- a/src/fastmcp/client/client.py
+++ b/src/fastmcp/client/client.py
@@ -77,6 +77,16 @@ logger = get_logger(__name__)
T = TypeVar("T", bound="ClientTransport")
+def _timeout_to_seconds(
+ timeout: datetime.timedelta | float | int | None,
+) -> float | None:
+ if timeout is None:
+ return None
+ if isinstance(timeout, datetime.timedelta):
+ return timeout.total_seconds()
+ return float(timeout)
+
+
@dataclass
class ClientSessionState:
"""Holds all session-related state for a Client instance.
@@ -222,6 +232,7 @@ class Client(Generic[ClientTransportT]):
message_handler: MessageHandlerT | MessageHandler | None = None,
progress_handler: ProgressHandler | None = None,
timeout: datetime.timedelta | float | int | None = None,
+ auto_initialize: bool = True,
init_timeout: datetime.timedelta | float | int | None = None,
client_info: mcp.types.Implementation | None = None,
auth: httpx.Auth | Literal["oauth"] | str | None = None,
@@ -240,26 +251,23 @@ class Client(Generic[ClientTransportT]):
self._progress_handler = progress_handler
+ # Convert timeout to timedelta if needed
if isinstance(timeout, int | float):
timeout = datetime.timedelta(seconds=float(timeout))
# handle init handshake timeout
if init_timeout is None:
init_timeout = fastmcp.settings.client_init_timeout
- if isinstance(init_timeout, datetime.timedelta):
- init_timeout = init_timeout.total_seconds()
- elif not init_timeout:
- init_timeout = None
- else:
- init_timeout = float(init_timeout)
- self._init_timeout = init_timeout
+ self._init_timeout = _timeout_to_seconds(init_timeout)
+
+ self.auto_initialize = auto_initialize
self._session_kwargs: SessionKwargs = {
"sampling_callback": None,
"list_roots_callback": None,
"logging_callback": create_log_callback(log_handler),
"message_handler": message_handler,
- "read_timeout_seconds": timeout,
+ "read_timeout_seconds": timeout, # ty: ignore[invalid-argument-type]
"client_info": client_info,
}
@@ -290,12 +298,8 @@ class Client(Generic[ClientTransportT]):
return self._session_state.session
@property
- def initialize_result(self) -> mcp.types.InitializeResult:
+ def initialize_result(self) -> mcp.types.InitializeResult | None:
"""Get the result of the initialization request."""
- if self._session_state.initialize_result is None:
- raise RuntimeError(
- "Client is not connected. Use the 'async with client:' context manager first."
- )
return self._session_state.initialize_result
def set_roots(self, roots: RootsList | RootsHandler) -> None:
@@ -357,15 +361,11 @@ class Client(Generic[ClientTransportT]):
self._session_state.session = session
# Initialize the session
try:
- with anyio.fail_after(self._init_timeout):
- self._session_state.initialize_result = (
- await self._session_state.session.initialize()
- )
+ if self.auto_initialize:
+ await self.initialize()
yield
except anyio.ClosedResourceError as e:
raise RuntimeError("Server session was closed unexpectedly") from e
- except TimeoutError as e:
- raise RuntimeError("Failed to initialize server session") from e
finally:
self._session_state.session = None
self._session_state.initialize_result = None
@@ -493,6 +493,55 @@ class Client(Generic[ClientTransportT]):
# --- MCP Client Methods ---
+ async def initialize(
+ self,
+ timeout: datetime.timedelta | float | int | None = None,
+ ) -> mcp.types.InitializeResult:
+ """Send an initialize request to the server.
+
+ This method performs the MCP initialization handshake with the server,
+ exchanging capabilities and server information. It is idempotent - calling
+ it multiple times returns the cached result from the first call.
+
+ The initialization happens automatically when entering the client context
+ manager unless `auto_initialize=False` was set during client construction.
+ Manual calls to this method are only needed when auto-initialization is disabled.
+
+ Args:
+ timeout: Optional timeout for the initialization request (seconds or timedelta).
+ If None, uses the client's init_timeout setting.
+
+ Returns:
+ InitializeResult: The server's initialization response containing server info,
+ capabilities, protocol version, and optional instructions.
+
+ Raises:
+ RuntimeError: If the client is not connected or initialization times out.
+
+ Example:
+ ```python
+ # With auto-initialization disabled
+ client = Client(server, auto_initialize=False)
+ async with client:
+ result = await client.initialize()
+ print(f"Server: {result.serverInfo.name}")
+ print(f"Instructions: {result.instructions}")
+ ```
+ """
+
+ if self.initialize_result is not None:
+ return self.initialize_result
+
+ if timeout is None:
+ timeout = self._init_timeout
+ try:
+ with anyio.fail_after(_timeout_to_seconds(timeout)):
+ initialize_result = await self.session.initialize()
+ self._session_state.initialize_result = initialize_result
+ return initialize_result
+ except TimeoutError as e:
+ raise RuntimeError("Failed to initialize server session") from e
+
async def ping(self) -> bool:
"""Send a ping request."""
result = await self.session.send_ping()
@@ -831,6 +880,7 @@ class Client(Generic[ClientTransportT]):
arguments: dict[str, Any],
progress_handler: ProgressHandler | None = None,
timeout: datetime.timedelta | float | int | None = None,
+ meta: dict[str, Any] | None = None,
) -> mcp.types.CallToolResult:
"""Send a tools/call request and return the complete MCP protocol result.
@@ -842,6 +892,10 @@ class Client(Generic[ClientTransportT]):
arguments (dict[str, Any]): Arguments to pass to the tool.
timeout (datetime.timedelta | float | int | None, optional): The timeout for the tool call. Defaults to None.
progress_handler (ProgressHandler | None, optional): The progress handler to use for the tool call. Defaults to None.
+ meta (dict[str, Any] | None, optional): Additional metadata to include with the request.
+ This is useful for passing contextual information (like user IDs, trace IDs, or preferences)
+ that shouldn't be tool arguments but may influence server-side processing. The server
+ can access this via `context.request_context.meta`. Defaults to None.
Returns:
mcp.types.CallToolResult: The complete response object from the protocol,
@@ -852,13 +906,16 @@ class Client(Generic[ClientTransportT]):
"""
logger.debug(f"[{self.name}] called call_tool: {name}")
+ # Convert timeout to timedelta if needed
if isinstance(timeout, int | float):
timeout = datetime.timedelta(seconds=float(timeout))
+
result = await self.session.call_tool(
name=name,
arguments=arguments,
- read_timeout_seconds=timeout,
+ read_timeout_seconds=timeout, # ty: ignore[invalid-argument-type]
progress_callback=progress_handler or self._progress_handler,
+ meta=meta,
)
return result
@@ -869,6 +926,7 @@ class Client(Generic[ClientTransportT]):
timeout: datetime.timedelta | float | int | None = None,
progress_handler: ProgressHandler | None = None,
raise_on_error: bool = True,
+ meta: dict[str, Any] | None = None,
) -> CallToolResult:
"""Call a tool on the server.
@@ -879,6 +937,11 @@ class Client(Generic[ClientTransportT]):
arguments (dict[str, Any] | None, optional): Arguments to pass to the tool. Defaults to None.
timeout (datetime.timedelta | float | int | None, optional): The timeout for the tool call. Defaults to None.
progress_handler (ProgressHandler | None, optional): The progress handler to use for the tool call. Defaults to None.
+ raise_on_error (bool, optional): Whether to raise a ToolError if the tool call results in an error. Defaults to True.
+ meta (dict[str, Any] | None, optional): Additional metadata to include with the request.
+ This is useful for passing contextual information (like user IDs, trace IDs, or preferences)
+ that shouldn't be tool arguments but may influence server-side processing. The server
+ can access this via `context.request_context.meta`. Defaults to None.
Returns:
CallToolResult:
@@ -898,6 +961,7 @@ class Client(Generic[ClientTransportT]):
arguments=arguments or {},
timeout=timeout,
progress_handler=progress_handler,
+ meta=meta,
)
data = None
if result.isError and raise_on_error:
@@ -928,6 +992,7 @@ class Client(Generic[ClientTransportT]):
return CallToolResult(
content=result.content,
structured_content=result.structuredContent,
+ meta=result.meta,
data=data,
is_error=result.isError,
)
@@ -945,5 +1010,6 @@ class Client(Generic[ClientTransportT]):
class CallToolResult:
content: list[mcp.types.ContentBlock]
structured_content: dict[str, Any] | None
+ meta: dict[str, Any] | None
data: Any = None
is_error: bool = False
diff --git a/src/fastmcp/client/transports.py b/src/fastmcp/client/transports.py
index 25f81afc4..81afc9c88 100644
--- a/src/fastmcp/client/transports.py
+++ b/src/fastmcp/client/transports.py
@@ -177,8 +177,8 @@ class SSETransport(ClientTransport):
self.url = url
self.headers = headers or {}
- self._set_auth(auth)
self.httpx_client_factory = httpx_client_factory
+ self._set_auth(auth)
if isinstance(sse_read_timeout, int | float):
sse_read_timeout = datetime.timedelta(seconds=float(sse_read_timeout))
@@ -186,7 +186,7 @@ class SSETransport(ClientTransport):
def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
if auth == "oauth":
- auth = OAuth(self.url)
+ auth = OAuth(self.url, httpx_client_factory=self.httpx_client_factory)
elif isinstance(auth, str):
auth = BearerAuth(auth)
self.auth = auth
@@ -247,8 +247,8 @@ class StreamableHttpTransport(ClientTransport):
self.url = url
self.headers = headers or {}
- self._set_auth(auth)
self.httpx_client_factory = httpx_client_factory
+ self._set_auth(auth)
if isinstance(sse_read_timeout, int | float):
sse_read_timeout = datetime.timedelta(seconds=float(sse_read_timeout))
@@ -256,7 +256,7 @@ class StreamableHttpTransport(ClientTransport):
def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
if auth == "oauth":
- auth = OAuth(self.url)
+ auth = OAuth(self.url, httpx_client_factory=self.httpx_client_factory)
elif isinstance(auth, str):
auth = BearerAuth(auth)
self.auth = auth
diff --git a/src/fastmcp/experimental/utilities/openapi/director.py b/src/fastmcp/experimental/utilities/openapi/director.py
index eb8b280fc..2efc8e74c 100644
--- a/src/fastmcp/experimental/utilities/openapi/director.py
+++ b/src/fastmcp/experimental/utilities/openapi/director.py
@@ -54,28 +54,27 @@ class RequestDirector:
url = self._build_url(route.path, path_params, base_url)
# Step 3: Prepare request data
- request_data = {
- "method": route.method.upper(),
- "url": url,
- "params": query_params if query_params else None,
- "headers": header_params if header_params else None,
- }
+ method: str = route.method.upper()
+ params = query_params if query_params else None
+ headers = header_params if header_params else None
+ json_body: dict[str, Any] | list[Any] | None = None
+ content: str | bytes | None = None
# Step 4: Handle request body
if body is not None:
if isinstance(body, dict | list):
- request_data["json"] = body
+ json_body = body
else:
- request_data["content"] = body
+ content = body
# Step 5: Create httpx.Request
return httpx.Request(
- method=request_data["method"],
- url=request_data["url"],
- params=request_data.get("params"),
- headers=request_data.get("headers"),
- json=request_data.get("json"),
- content=request_data.get("content"),
+ method=method,
+ url=url,
+ params=params,
+ headers=headers,
+ json=json_body,
+ content=content,
)
def _unflatten_arguments(
diff --git a/src/fastmcp/mcp_config.py b/src/fastmcp/mcp_config.py
index 878d60faf..4d8d8283b 100644
--- a/src/fastmcp/mcp_config.py
+++ b/src/fastmcp/mcp_config.py
@@ -101,7 +101,7 @@ class _TransformingMCPServerMixin(FastMCPBaseModel):
ClientTransport, # pyright: ignore[reportUnusedImport]
)
- transport: ClientTransport = super().to_transport() # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownVariableType]
+ transport: ClientTransport = super().to_transport() # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownVariableType] # ty: ignore[unresolved-attribute]
transport = cast(ClientTransport, transport)
client: Client[ClientTransport] = Client(transport=transport, name=client_name)
diff --git a/src/fastmcp/server/auth/auth.py b/src/fastmcp/server/auth/auth.py
index adae95b7d..814f905f0 100644
--- a/src/fastmcp/server/auth/auth.py
+++ b/src/fastmcp/server/auth/auth.py
@@ -1,6 +1,6 @@
from __future__ import annotations
-from typing import Any
+from typing import Any, cast
from mcp.server.auth.middleware.auth_context import AuthContextMiddleware
from mcp.server.auth.middleware.bearer_auth import BearerAuthBackend
@@ -28,6 +28,10 @@ from starlette.middleware import Middleware
from starlette.middleware.authentication import AuthenticationMiddleware
from starlette.routing import Route
+from fastmcp.utilities.logging import get_logger
+
+logger = get_logger(__name__)
+
class AccessToken(_SDKAccessToken):
"""AccessToken that includes all JWT claims."""
@@ -294,20 +298,27 @@ class OAuthProvider(
required_scopes: Scopes that are required for all requests.
"""
- # Convert URLs to proper types
- if isinstance(base_url, str):
- base_url = AnyHttpUrl(base_url)
-
super().__init__(base_url=base_url, required_scopes=required_scopes)
- self.base_url = base_url
if issuer_url is None:
- self.issuer_url = base_url
+ self.issuer_url = self.base_url
elif isinstance(issuer_url, str):
self.issuer_url = AnyHttpUrl(issuer_url)
else:
self.issuer_url = issuer_url
+ # Log if issuer_url and base_url differ (requires additional setup)
+ if (
+ self.base_url is not None
+ and self.issuer_url is not None
+ and str(self.base_url) != str(self.issuer_url)
+ ):
+ logger.info(
+ f"OAuth endpoints at {self.base_url}, issuer at {self.issuer_url}. "
+ f"Ensure well-known routes are accessible at root ({self.issuer_url}/.well-known/). "
+ f"See: https://gofastmcp.com/deployment/http#mounting-authenticated-servers"
+ )
+
# Initialize OAuth Authorization Server Provider
OAuthAuthorizationServerProvider.__init__(self)
@@ -348,9 +359,17 @@ class OAuthProvider(
"""
# Create standard OAuth authorization server routes
+ # Pass base_url as issuer_url to ensure metadata declares endpoints where
+ # they're actually accessible (operational routes are mounted at
+ # base_url)
+ assert self.base_url is not None # typing check
+ assert (
+ self.issuer_url is not None
+ ) # typing check (issuer_url defaults to base_url)
+
oauth_routes = create_auth_routes(
provider=self,
- issuer_url=self.issuer_url,
+ issuer_url=self.base_url,
service_documentation_url=self.service_documentation_url,
client_registration_options=self.client_registration_options,
revocation_options=self.revocation_options,
@@ -369,7 +388,7 @@ class OAuthProvider(
)
protected_routes = create_protected_resource_routes(
resource_url=resource_url,
- authorization_servers=[self.issuer_url],
+ authorization_servers=[cast(AnyHttpUrl, self.issuer_url)],
scopes_supported=supported_scopes,
)
oauth_routes.extend(protected_routes)
diff --git a/src/fastmcp/server/auth/handlers/authorize.py b/src/fastmcp/server/auth/handlers/authorize.py
index 13c98a56d..f91b266b7 100644
--- a/src/fastmcp/server/auth/handlers/authorize.py
+++ b/src/fastmcp/server/auth/handlers/authorize.py
@@ -13,6 +13,7 @@ The enhancement adds:
from __future__ import annotations
+import json
from typing import TYPE_CHECKING
from mcp.server.auth.handlers.authorize import (
@@ -211,12 +212,15 @@ class AuthorizationHandler(SDKAuthorizationHandler):
# Check if this is a client not found error
if response.status_code == 400:
# Try to extract client_id from request for enhanced error
- client_id = None
+ client_id: str | None = None
if request.method == "GET":
client_id = request.query_params.get("client_id")
else:
form = await request.form()
- client_id = form.get("client_id")
+ client_id_value = form.get("client_id")
+ # Ensure client_id is a string, not UploadFile
+ if isinstance(client_id_value, str):
+ client_id = client_id_value
# If we have a client_id and the error is about it not being found,
# enhance the response
@@ -224,9 +228,7 @@ class AuthorizationHandler(SDKAuthorizationHandler):
try:
# Check if response body contains "not found" error
if hasattr(response, "body"):
- import json
-
- body = json.loads(response.body)
+ body = json.loads(bytes(response.body))
if (
body.get("error") == "invalid_request"
and "not found" in body.get("error_description", "").lower()
diff --git a/src/fastmcp/server/auth/oauth_proxy.py b/src/fastmcp/server/auth/oauth_proxy.py
index 6f1336d96..a1452378b 100644
--- a/src/fastmcp/server/auth/oauth_proxy.py
+++ b/src/fastmcp/server/auth/oauth_proxy.py
@@ -309,10 +309,13 @@ def create_consent_html(
"""
# Build form with buttons
+ # Use empty action to submit to current URL (/consent or /mcp/consent)
+ # The POST handler is registered at the same path as GET
form = f"""
-