Fix merge conflicts in context.py

This commit is contained in:
Jeremiah Lowin 2025-11-02 16:48:33 -05:00
commit 38af6228f4
120 changed files with 2280 additions and 1087 deletions

View file

@ -74,5 +74,6 @@ jobs:
"model": "claude-sonnet-4-5-20250929",
"env": {
"GH_TOKEN": "${{ steps.marvin-token.outputs.token }}"
}
},
"customInstructions": "When you complete work on an issue: (1) You MUST create a pull request using the mcp__github__create_pull_request tool instead of posting a link, and (2) You MUST add the 'marvin-pr' label to the original issue using mcp__github__update_issue. Even if PR creation fails and you post a link instead, you MUST still add the 'marvin-pr' label. Follow the PR message guidelines in CLAUDE.md."
}

View file

@ -59,6 +59,31 @@ jobs:
- name: Run client process tests separately
run: uv run pytest --inline-snapshot=disable tests -m "client_process" -x
run_tests_lowest_direct:
name: "Run tests with lowest-direct dependencies"
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: "3.10"
- name: Install FastMCP with lowest-direct resolution
# run with lowest-direct to test against the minimum allowed dependency versions
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
- name: Run client process tests separately
run: uv run pytest --inline-snapshot=disable tests -m "client_process" -x
run_integration_tests:
name: "Run integration tests"
runs-on: ubuntu-latest

View file

@ -4,6 +4,40 @@ icon: "list-check"
rss: true
---
<Update label="v2.13.0" description="2025-10-25">
**[v2.13.0: Cache Me If You Can](https://github.com/jlowin/fastmcp/releases/tag/v2.13.0)**
FastMCP 2.13 "Cache Me If You Can" represents a fundamental maturation of the framework. After months of community feedback on authentication and state management, this release delivers the infrastructure FastMCP needs to handle production workloads: persistent storage, response caching, and pragmatic OAuth improvements that reflect real-world deployment challenges.
💾 **Pluggable storage backends** bring persistent state to FastMCP servers. Built on [py-key-value-aio](https://github.com/strawgate/py-key-value), a new library from FastMCP maintainer Bill Easton ([@strawgate](https://github.com/strawgate)), the storage layer provides encrypted disk storage by default, platform-aware token management, and a simple key-value interface for application state. We're excited to bring this elegantly designed library into the FastMCP ecosystem - it's both powerful and remarkably easy to use, including wrappers to add encryption, TTLs, caching, and more to backends ranging from Elasticsearch, Redis, DynamoDB, filesystem, in-memory, and more! OAuth providers now automatically persist tokens across restarts, and developers can store arbitrary state without reaching for external databases. This foundation enables long-running sessions, cached credentials, and stateful applications built on MCP.
🔐 **OAuth maturity** brings months of production learnings into the framework. The new consent screen prevents confused deputy and authorization bypass attacks discovered in earlier versions while providing a clean UX with customizable branding. The OAuth proxy now issues its own tokens with automatic key derivation from client secrets, and RFC 7662 token introspection support enables enterprise auth flows. Path prefix mounting enables OAuth-protected servers to integrate into existing web applications under custom paths like `/api`, and MCP 1.17+ compliance with RFC 9728 ensures protocol compatibility. Combined with improved error handling and platform-aware token storage, OAuth is now production-ready and security-hardened for serious applications.
FastMCP now supports out-of-the-box authentication with:
- **[WorkOS](https://gofastmcp.com/integrations/workos)** and **[AuthKit](https://gofastmcp.com/integrations/authkit)**
- **[GitHub](https://gofastmcp.com/integrations/github)**
- **[Google](https://gofastmcp.com/integrations/google)**
- **[Azure](https://gofastmcp.com/integrations/azure)** (Entra ID)
- **[AWS Cognito](https://gofastmcp.com/integrations/aws-cognito)**
- **[Auth0](https://gofastmcp.com/integrations/auth0)**
- **[Descope](https://gofastmcp.com/integrations/descope)**
- **[Scalekit](https://gofastmcp.com/integrations/scalekit)**
- **[JWTs](https://gofastmcp.com/servers/auth/token-verification#jwt-token-verification)**
- **[RFC 7662 token introspection](https://gofastmcp.com/servers/auth/token-verification#token-introspection-protocol)**
⚡ **Response Caching Middleware** dramatically improves performance for expensive operations. Cache tool and resource responses with configurable TTLs, reducing redundant API calls and speeding up repeated queries.
🔄 **Server lifespans** provide proper initialization and cleanup hooks that run once per server instance instead of per client session. This fixes a long-standing source of confusion in the MCP SDK and enables proper resource management for database connections, background tasks, and other server-level state. Note: this is a breaking behavioral change if you were using the `lifespan` parameter.
✨ **Developer experience improvements** include Pydantic input validation for better type safety, icon support for richer UX, RFC 6570 query parameters for resource templates, improved Context API methods (list_resources, list_prompts, get_prompt), and async file/directory resources.
This release includes contributions from **20** new contributors and represents the largest feature set in a while. Thank you to everyone who tested preview builds and filed issues - your feedback shaped these improvements!
**Full Changelog**: [v2.12.5...v2.13.0](https://github.com/jlowin/fastmcp/compare/v2.12.5...v2.13.0)
</Update>
<Update label="v2.12.5" description="2025-10-17">
**[v2.12.5: Safety Pin](https://github.com/jlowin/fastmcp/releases/tag/v2.12.5)**

View file

@ -53,7 +53,7 @@ async with Client("https://fastmcp.cloud/mcp", auth=oauth) as client:
- **`mcp_url`** (`str`): The full URL of the target MCP server endpoint. Used to discover OAuth server metadata
- **`scopes`** (`str | list[str]`, optional): OAuth scopes to request. Can be space-separated string or list of strings
- **`client_name`** (`str`, optional): Client name for dynamic registration. Defaults to `"FastMCP Client"`
- **`token_storage_cache_dir`** (`Path`, optional): Token cache directory. Defaults to `~/.fastmcp/oauth-mcp-client-cache/`
- **`token_storage`** (`AsyncKeyValue`, optional): Storage backend for persisting OAuth tokens. Defaults to in-memory storage (tokens lost on restart). See [Token Storage](#token-storage) for encrypted storage options
- **`additional_client_metadata`** (`dict[str, Any]`, optional): Extra metadata for client registration
- **`callback_port`** (`int`, optional): Fixed port for OAuth callback server. If not specified, uses a random available port
@ -64,7 +64,7 @@ The OAuth flow is triggered when you use a FastMCP `Client` configured to use OA
<Steps>
<Step title="Token Check">
The client first checks the `token_storage_cache_dir` for existing, valid tokens for the target server. If one is found, it will be used to authenticate the client.
The client first checks the configured `token_storage` backend for existing, valid tokens for the target server. If one is found, it will be used to authenticate the client.
</Step>
<Step title="OAuth Server Discovery">
If no valid tokens exist, the client attempts to discover the OAuth server's endpoints using a well-known URI (e.g., `/.well-known/oauth-authorization-server`) based on the `mcp_url`.
@ -82,37 +82,51 @@ The user's default web browser is automatically opened, directing them to the OA
Upon approval, the OAuth server redirects the user's browser to the local callback server with an `authorization_code`. The client captures this code and exchanges it with the OAuth server's token endpoint for an `access_token` (and often a `refresh_token`) using PKCE for security.
</Step>
<Step title="Token Caching">
The obtained tokens are saved to the `token_storage_cache_dir` for future use, eliminating the need for repeated browser interactions.
</Step>
The obtained tokens are saved to the configured `token_storage` backend for future use, eliminating the need for repeated browser interactions.
</Step>
<Step title="Authenticated Requests">
The access token is automatically included in the `Authorization` header for requests to the MCP server.
The access token is automatically included in the `Authorization` header for requests to the MCP server.
</Step>
<Step title="Refresh Token">
If the access token expires, the client will automatically use the refresh token to get a new access token.
</Step>
</Steps>
## Token Management
## Token Storage
### Token Storage
<VersionBadge version="2.13.0" />
OAuth access tokens are automatically cached in `~/.fastmcp/oauth-mcp-client-cache/` and persist between application runs. Files are keyed by the OAuth server's base URL.
By default, tokens are stored in memory and lost when your application restarts. For persistent storage, pass an `AsyncKeyValue`-compatible storage backend to the `token_storage` parameter.
### Managing Cache
To clear the tokens for a specific server, instantiate a `FileTokenStorage` instance and call the `clear` method:
<Warning>
**Security Consideration**: Use encrypted storage for production. MCP clients can accumulate OAuth credentials for many servers over time, and a compromised token store could expose access to multiple services.
</Warning>
```python
from fastmcp.client.auth.oauth import FileTokenStorage
from fastmcp import Client
from fastmcp.client.auth import OAuth
from key_value.aio.stores.disk import DiskStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
import os
storage = FileTokenStorage(server_url="https://fastmcp.cloud/mcp")
await storage.clear()
# Create encrypted disk storage
encrypted_storage = FernetEncryptionWrapper(
key_value=DiskStore(directory="~/.fastmcp/oauth-tokens"),
fernet=Fernet(os.environ["OAUTH_STORAGE_ENCRYPTION_KEY"])
)
oauth = OAuth(
mcp_url="https://fastmcp.cloud/mcp",
token_storage=encrypted_storage
)
async with Client("https://fastmcp.cloud/mcp", auth=oauth) as client:
await client.ping()
```
To clear *all* tokens for all servers, call the `clear_all` method on the `FileTokenStorage` class:
You can use any `AsyncKeyValue`-compatible backend from the [key-value library](https://github.com/strawgate/py-key-value) including Redis, DynamoDB, and more. Wrap your storage in `FernetEncryptionWrapper` for encryption.
```python
from fastmcp.client.auth.oauth import FileTokenStorage
FileTokenStorage.clear_all()
```
<Note>
When selecting a storage backend, review the [py-key-value documentation](https://github.com/strawgate/py-key-value) to understand the maturity level and limitations of your chosen backend. Some backends may be in preview or have constraints that affect production suitability.
</Note>

View file

@ -45,7 +45,7 @@ Run your server with a simple Python command:
python server.py
```
Your server is now accessible at `http://localhost:8000/mcp/` (or use your server's actual IP address for remote access).
Your server is now accessible at `http://localhost:8000/mcp` (or use your server's actual IP address for remote access).
This approach is ideal when you want to get online quickly with minimal configuration. It's perfect for internal tools, development environments, or simple deployments where you don't need advanced server features. The built-in server handles all the HTTP details, letting you focus on your MCP implementation.
@ -72,7 +72,7 @@ Run with any ASGI server - here's an example with Uvicorn:
uvicorn app:app --host 0.0.0.0 --port 8000
```
Your server is accessible at the same URL: `http://localhost:8000/mcp/` (or use your server's actual IP address for remote access).
Your server is accessible at the same URL: `http://localhost:8000/mcp` (or use your server's actual IP address for remote access).
The ASGI approach shines in production environments where you need reliability and performance. You can run multiple worker processes to handle concurrent requests, add custom middleware for logging or monitoring, integrate with existing deployment pipelines, or mount your MCP server as part of a larger application.
@ -293,7 +293,7 @@ api.mount("/mcp", mcp.http_app())
# Run with: uvicorn app:api --host 0.0.0.0 --port 8000
```
Your existing API remains at `http://localhost:8000/api/` while MCP is available at `http://localhost:8000/mcp/`.
Your existing API remains at `http://localhost:8000/api` while MCP is available at `http://localhost:8000/mcp`.
## Mounting Authenticated Servers

View file

@ -83,7 +83,7 @@ if __name__ == "__main__":
mcp.run(transport="http", host="127.0.0.1", port=8000)
```
Your server is now accessible at `http://localhost:8000/mcp/`. This URL is the MCP endpoint that clients will connect to. HTTP transport enables:
Your server is now accessible at `http://localhost:8000/mcp`. This URL is the MCP endpoint that clients will connect to. HTTP transport enables:
- Network accessibility
- Multiple concurrent clients
- Integration with web infrastructure

View file

@ -20,7 +20,10 @@
"primary": "#2d00f7"
},
"contextual": {
"options": ["copy", "view"]
"options": [
"copy",
"view"
]
},
"description": "The fast, Pythonic way to build MCP servers and clients.",
"errors": {
@ -146,7 +149,10 @@
{
"group": "Essentials",
"icon": "cube",
"pages": ["clients/client", "clients/transports"]
"pages": [
"clients/client",
"clients/transports"
]
},
{
"group": "Core Operations",
@ -172,7 +178,10 @@
{
"group": "Authentication",
"icon": "user-shield",
"pages": ["clients/auth/oauth", "clients/auth/bearer"]
"pages": [
"clients/auth/oauth",
"clients/auth/bearer"
]
}
]
},
@ -226,7 +235,10 @@
{
"group": "API Integration",
"icon": "globe",
"pages": ["integrations/fastapi", "integrations/openapi"]
"pages": [
"integrations/fastapi",
"integrations/openapi"
]
}
]
},
@ -333,6 +345,7 @@
"python-sdk/fastmcp-server-auth-__init__",
"python-sdk/fastmcp-server-auth-auth",
"python-sdk/fastmcp-server-auth-jwt_issuer",
"python-sdk/fastmcp-server-auth-middleware",
"python-sdk/fastmcp-server-auth-oauth_proxy",
"python-sdk/fastmcp-server-auth-oidc_proxy",
{
@ -371,7 +384,8 @@
"python-sdk/fastmcp-server-middleware-logging",
"python-sdk/fastmcp-server-middleware-middleware",
"python-sdk/fastmcp-server-middleware-rate_limiting",
"python-sdk/fastmcp-server-middleware-timing"
"python-sdk/fastmcp-server-middleware-timing",
"python-sdk/fastmcp-server-middleware-tool_injection"
]
},
"python-sdk/fastmcp-server-openapi",
@ -458,17 +472,17 @@
"search": {
"prompt": "Search the docs..."
},
"styling": {
"codeblocks": {
"theme": {
"dark": "dark-plus",
"light": "snazzy-light"
}
}
},
"theme": "almond",
"thumbnails": {
"appearance": "light",
"background": "/assets/brand/thumbnail-background.png"
},
"styling": {
"codeblocks": {
"theme": {
"light": "snazzy-light",
"dark": "dark-plus"
}
}
}
}

View file

@ -71,8 +71,9 @@ FastMCP handles all the complex protocol details so you can focus on building. I
FastMCP provides the shortest path from idea to production. Deploy locally, to the cloud with [FastMCP Cloud](https://fastmcp.cloud) (free for personal servers), or to your own infrastructure.
<Tip>
**This documentation reflects FastMCP's `main` branch**, meaning it always reflects the latest development version. Features are generally marked with version badges (e.g. `New in version: 2.13.1`) to indicate when they were introduced. Note that this may include features that are not yet released.
</Tip>
## LLM-Friendly Docs

View file

@ -132,7 +132,7 @@ import asyncio
async def main():
# The client will automatically handle Auth0 OAuth flows
async with Client("http://localhost:8000/mcp/", auth="oauth") as client:
async with Client("http://localhost:8000/mcp", auth="oauth") as client:
# First-time connection will open Auth0 login in your browser
print("✓ Authenticated with Auth0!")

View file

@ -70,7 +70,7 @@ from fastmcp import Client
import asyncio
async def main():
async with Client("http://localhost:8000/mcp/", auth="oauth") as client:
async with Client("http://localhost:8000/mcp", auth="oauth") as client:
assert await client.ping()
if __name__ == "__main__":

View file

@ -170,7 +170,7 @@ import asyncio
async def main():
# The client will automatically handle AWS Cognito OAuth
async with Client("http://localhost:8000/mcp/", auth="oauth") as client:
async with Client("http://localhost:8000/mcp", auth="oauth") as client:
# First-time connection will open AWS Cognito login in your browser
print("✓ Authenticated with AWS Cognito!")

View file

@ -1,6 +1,6 @@
---
title: Azure (Microsoft Entra) OAuth 🤝 FastMCP
sidebarTitle: Azure
title: Azure (Microsoft Entra ID) OAuth 🤝 FastMCP
sidebarTitle: Azure (Entra ID)
description: Secure your FastMCP server with Azure/Microsoft Entra OAuth
icon: microsoft
tag: NEW
@ -122,12 +122,13 @@ auth_provider = AzureProvider(
client_secret="your-client-secret", # Your Azure App Client Secret
tenant_id="08541b6e-646d-43de-a0eb-834e6713d6d5", # Your Azure Tenant ID (REQUIRED)
base_url="http://localhost:8000", # Must match your App registration
required_scopes=["your-scope"], # Name of scope created when configuring your App
required_scopes=["your-scope"], # At least one scope REQUIRED - name of scope from your App
# identifier_uri defaults to api://{client_id}
# identifier_uri="api://your-api-id",
# Optional: request additional upstream scopes in the authorize request
# additional_authorize_scopes=["User.Read", "offline_access", "openid", "email"],
# redirect_path="/auth/callback" # Default value, customize if needed
# base_authority="login.microsoftonline.us" # For Azure Government (default: login.microsoftonline.com)
)
mcp = FastMCP(name="Azure Secured App", auth=auth_provider)
@ -159,6 +160,10 @@ async def get_user_info() -> dict:
Using your specific tenant ID is recommended for better security and control.
</Note>
<Note>
**Important**: The `required_scopes` parameter is **REQUIRED** and must include at least one scope. Azure's OAuth API requires the `scope` parameter in all authorization requests - you cannot authenticate without specifying at least one scope. Use the unprefixed scope names from your Azure App registration (e.g., `["read", "write"]`). These scopes must be created under **Expose an API** in your App registration.
</Note>
## Testing
### Running the Server
@ -181,7 +186,7 @@ import asyncio
async def main():
# The client will automatically handle Azure OAuth
async with Client("http://localhost:8000/mcp/", auth="oauth") as client:
async with Client("http://localhost:8000/mcp", auth="oauth") as client:
# First-time connection will open Azure login in your browser
print("✓ Authenticated with Azure!")
@ -296,8 +301,12 @@ Issuer URL for OAuth metadata (defaults to `BASE_URL`). Set to root-level URL wh
Redirect path configured in your Azure App registration
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_AZURE_REQUIRED_SCOPES" default="">
Comma-, space-, or JSON-separated list of required scopes for your API. These are validated on tokens and used as defaults if the client does not request specific scopes.
<ParamField path="FASTMCP_SERVER_AUTH_AZURE_REQUIRED_SCOPES" required>
Comma-, space-, or JSON-separated list of required scopes for your API (at least one scope required). These are validated on tokens and used as defaults if the client does not request specific scopes. Use unprefixed scope names from your Azure App registration (e.g., `read,write`).
<Note>
Azure's OAuth API requires the `scope` parameter - you must provide at least one scope.
</Note>
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_AZURE_ADDITIONAL_AUTHORIZE_SCOPES" default="">
@ -307,6 +316,15 @@ Comma-, space-, or JSON-separated list of additional scopes to include in the au
<ParamField path="FASTMCP_SERVER_AUTH_AZURE_IDENTIFIER_URI" default="api://{client_id}">
Application ID URI used to prefix scopes during authorization.
</ParamField>
<ParamField path="FASTMCP_SERVER_AUTH_AZURE_BASE_AUTHORITY" default="login.microsoftonline.com">
Azure authority base URL. Override this to use Azure Government:
- `login.microsoftonline.com` - Azure Public Cloud (default)
- `login.microsoftonline.us` - Azure Government
This setting affects all Azure OAuth endpoints (authorization, token, issuer, JWKS).
</ParamField>
</Card>
Example `.env` file:

View file

@ -93,7 +93,7 @@ from fastmcp import Client
import asyncio
async def main():
async with Client("http://localhost:8000/mcp/", auth="oauth") as client:
async with Client("http://localhost:8000/mcp", auth="oauth") as client:
assert await client.ping()
if __name__ == "__main__":

View file

@ -366,7 +366,7 @@ combined_app = FastAPI(
# Now you have:
# - Regular API: http://localhost:8000/products
# - LLM-friendly MCP: http://localhost:8000/mcp/
# - LLM-friendly MCP: http://localhost:8000/mcp
# Both served from the same FastAPI application!
```

View file

@ -114,7 +114,7 @@ import asyncio
async def main():
# The client will automatically handle GitHub OAuth
async with Client("http://localhost:8000/mcp/", auth="oauth") as client:
async with Client("http://localhost:8000/mcp", auth="oauth") as client:
# First-time connection will open GitHub login in your browser
print("✓ Authenticated with GitHub!")

View file

@ -125,7 +125,7 @@ import asyncio
async def main():
# The client will automatically handle Google OAuth
async with Client("http://localhost:8000/mcp/", auth="oauth") as client:
async with Client("http://localhost:8000/mcp", auth="oauth") as client:
# First-time connection will open Google login in your browser
print("✓ Authenticated with Google!")

View file

@ -105,7 +105,7 @@ fastmcp inspect # auto-detect fastmcp.json
- `server_spec`: Python file to inspect, optionally with \:object suffix, or fastmcp.json
### `prepare` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/cli.py#L785" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `prepare` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/cli.py#L786" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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

View file

@ -54,7 +54,7 @@ Set is_binary=True to read file as binary data instead of text.
**Methods:**
#### `validate_absolute_path` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/resources/types.py#L59" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `validate_absolute_path` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/resources/types.py#L63" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
validate_absolute_path(cls, path: Path) -> Path
@ -63,7 +63,7 @@ validate_absolute_path(cls, path: Path) -> Path
Ensure path is absolute.
#### `set_binary_from_mime_type` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/resources/types.py#L67" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `set_binary_from_mime_type` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/resources/types.py#L71" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
set_binary_from_mime_type(cls, is_binary: bool, info: ValidationInfo) -> bool
@ -72,7 +72,7 @@ set_binary_from_mime_type(cls, is_binary: bool, info: ValidationInfo) -> bool
Set is_binary based on mime_type if not explicitly set.
#### `read` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/resources/types.py#L74" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `read` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/resources/types.py#L79" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
read(self) -> str | bytes
@ -81,7 +81,7 @@ read(self) -> str | bytes
Read the file content.
### `HttpResource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/resources/types.py#L84" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `HttpResource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/resources/types.py#L89" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
A resource that reads from an HTTP endpoint.
@ -89,7 +89,7 @@ A resource that reads from an HTTP endpoint.
**Methods:**
#### `read` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/resources/types.py#L92" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `read` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/resources/types.py#L98" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
read(self) -> str | bytes
@ -98,7 +98,7 @@ read(self) -> str | bytes
Read the HTTP content.
### `DirectoryResource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/resources/types.py#L100" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `DirectoryResource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/resources/types.py#L106" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
A resource that lists files in a directory.
@ -106,7 +106,7 @@ A resource that lists files in a directory.
**Methods:**
#### `validate_absolute_path` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/resources/types.py#L116" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `validate_absolute_path` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/resources/types.py#L126" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
validate_absolute_path(cls, path: Path) -> Path
@ -115,7 +115,7 @@ validate_absolute_path(cls, path: Path) -> Path
Ensure path is absolute.
#### `list_files` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/resources/types.py#L122" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `list_files` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/resources/types.py#L132" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_files(self) -> list[Path]
@ -124,7 +124,7 @@ list_files(self) -> list[Path]
List files in the directory.
#### `read` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/resources/types.py#L144" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `read` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/resources/types.py#L148" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
read(self) -> str

View file

@ -15,69 +15,19 @@ This maintains proper OAuth 2.0 token audience boundaries.
## Functions
### `derive_jwt_key` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/jwt_issuer.py#L25" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `derive_jwt_key` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/jwt_issuer.py#L37" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
derive_jwt_key(from_secret: str, server_salt: str) -> bytes
derive_jwt_key() -> bytes
```
Derive JWT signing key from upstream client secret and server salt.
Uses HKDF (RFC 5869) to derive a cryptographically secure signing key from
the upstream OAuth client secret combined with a server-specific salt.
**Args:**
- `from_secret`: The OAuth client secret from upstream provider
- `server_salt`: Random salt unique to this server instance
**Returns:**
- 32-byte key suitable for HS256 JWT signing
### `derive_encryption_key` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/jwt_issuer.py#L46" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
derive_encryption_key(from_secret: str) -> bytes
```
Derive Fernet encryption key from upstream client secret.
Uses HKDF to derive a cryptographically secure encryption key for
encrypting upstream tokens at rest.
**Args:**
- `from_secret`: The OAuth client secret from upstream provider
**Returns:**
- 32-byte Fernet key (base64url-encoded)
### `derive_key_from_secret` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/jwt_issuer.py#L67" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
derive_key_from_secret(secret: str | bytes, salt: str, info: bytes) -> bytes
```
Derive 32-byte key from user-provided secret (string or bytes).
Accepts any length input and derives a proper cryptographic key.
Uses HKDF to stretch weak inputs into strong keys.
**Args:**
- `secret`: User-provided secret (any string or bytes)
- `salt`: Application-specific salt string
- `info`: Key purpose identifier
**Returns:**
- 32-byte key suitable for HS256 JWT signing or Fernet encryption
Derive JWT signing key from a high-entropy or low-entropy key material and server salt.
## Classes
### `JWTIssuer` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/jwt_issuer.py#L90" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `JWTIssuer` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/jwt_issuer.py#L74" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Issues and validates FastMCP-signed JWT tokens using HS256.
@ -89,7 +39,7 @@ a key derived from the upstream client secret.
**Methods:**
#### `issue_access_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/jwt_issuer.py#L116" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `issue_access_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/jwt_issuer.py#L100" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
issue_access_token(self, client_id: str, scopes: list[str], jti: str, expires_in: int = 3600) -> str
@ -111,7 +61,7 @@ which contains actual user identity and authorization data.
- Signed JWT token
#### `issue_refresh_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/jwt_issuer.py#L163" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `issue_refresh_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/jwt_issuer.py#L147" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
issue_refresh_token(self, client_id: str, scopes: list[str], jti: str, expires_in: int) -> str
@ -133,7 +83,7 @@ token which contains actual user identity and authorization data.
- Signed JWT token
#### `verify_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/jwt_issuer.py#L211" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `verify_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/jwt_issuer.py#L195" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
verify_token(self, token: str) -> dict[str, Any]
@ -152,44 +102,3 @@ Validates JWT signature, expiration, issuer, and audience.
**Raises:**
- `JoseError`: If token is invalid, expired, or has wrong claims
### `TokenEncryption` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/jwt_issuer.py#L255" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Handles encryption/decryption of upstream OAuth tokens at rest.
**Methods:**
#### `encrypt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/jwt_issuer.py#L266" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
encrypt(self, token: str) -> bytes
```
Encrypt a token for storage.
**Args:**
- `token`: Plain text token
**Returns:**
- Encrypted token bytes
#### `decrypt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/jwt_issuer.py#L277" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
decrypt(self, encrypted_token: bytes) -> str
```
Decrypt a token from storage.
**Args:**
- `encrypted_token`: Encrypted token bytes
**Returns:**
- Plain text token
**Raises:**
- `cryptography.fernet.InvalidToken`: If token is corrupted or key is wrong

View file

@ -0,0 +1,26 @@
---
title: middleware
sidebarTitle: middleware
---
# `fastmcp.server.auth.middleware`
Enhanced authentication middleware with better error messages.
This module provides enhanced versions of MCP SDK authentication middleware
that return more helpful error messages for developers troubleshooting
authentication issues.
## Classes
### `RequireAuthMiddleware` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/middleware.py#L22" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Enhanced authentication middleware with detailed error messages.
Extends the SDK's RequireAuthMiddleware to provide more actionable
error messages when authentication fails. This helps developers
understand what went wrong and how to fix it.

View file

@ -26,7 +26,7 @@ production use with enterprise identity providers.
## Functions
### `create_consent_html` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L230" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `create_consent_html` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L236" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
create_consent_html(client_id: str, redirect_uri: str, scopes: list[str], txn_id: str, csrf_token: str, client_name: str | None = None, title: str = 'Application Access Request', server_name: str | None = None, server_icon_url: str | None = None, server_website_url: str | None = None, client_website_url: str | None = None) -> str
@ -38,7 +38,7 @@ Create a styled HTML consent page for OAuth authorization requests.
## Classes
### `OAuthTransaction` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L103" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `OAuthTransaction` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L108" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
OAuth transaction state for consent flow.
@ -47,7 +47,7 @@ Stored server-side to track active authorization flows with client context.
Includes CSRF tokens for consent protection per MCP security best practices.
### `ClientCode` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L124" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `ClientCode` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L129" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Client authorization code with PKCE and upstream tokens.
@ -56,16 +56,17 @@ Stored server-side after upstream IdP callback. Contains the upstream
tokens bound to the client's PKCE challenge for secure token exchange.
### `UpstreamTokenSet` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L142" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `UpstreamTokenSet` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L147" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Stored upstream OAuth tokens from identity provider.
These tokens are obtained from the upstream provider (Google, GitHub, etc.)
and are stored encrypted at rest. They are never exposed to MCP clients.
and stored in plaintext within this model. Encryption is handled transparently
at the storage layer via FernetEncryptionWrapper. Tokens are never exposed to MCP clients.
### `JTIMapping` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L163" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `JTIMapping` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L169" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Maps FastMCP token JTI to upstream token ID.
@ -74,7 +75,7 @@ This allows stateless JWT validation while still being able to look up
the corresponding upstream token when tools need to access upstream APIs.
### `ProxyDCRClient` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L175" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `ProxyDCRClient` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L181" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Client for DCR proxy with configurable redirect URI validation.
@ -104,7 +105,7 @@ arise from accepting arbitrary redirect URIs.
**Methods:**
#### `validate_redirect_uri` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L204" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `validate_redirect_uri` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L210" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
validate_redirect_uri(self, redirect_uri: AnyUrl | None) -> AnyUrl
@ -118,7 +119,7 @@ This is essential for cached token scenarios where the client may
reconnect with a different port.
### `TokenHandler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L377" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `TokenHandler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L383" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
TokenHandler that returns OAuth 2.1 compliant error responses.
@ -141,7 +142,7 @@ Per MCP spec: "Invalid or expired tokens MUST receive a HTTP 401 response."
**Methods:**
#### `response` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L396" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `response` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L402" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
response(self, obj: TokenSuccessResponse | TokenErrorResponse)
@ -150,7 +151,7 @@ response(self, obj: TokenSuccessResponse | TokenErrorResponse)
Override response method to provide OAuth 2.1 compliant error handling.
### `OAuthProxy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L426" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `OAuthProxy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L432" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
OAuth provider that presents a DCR-compliant interface while proxying to non-DCR IDPs.
@ -260,7 +261,7 @@ Handles provider-specific requirements:
**Methods:**
#### `get_client` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L860" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_client` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L813" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_client(self, client_id: str) -> OAuthClientInformationFull | None
@ -272,7 +273,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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L875" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `register_client` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L829" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
register_client(self, client_info: OAuthClientInformationFull) -> None
@ -286,7 +287,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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L921" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `authorize` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L876" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str
@ -303,7 +304,7 @@ If consent is disabled (require_authorization_consent=False), skip the consent s
and redirect directly to the upstream IdP.
#### `load_authorization_code` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L995" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `load_authorization_code` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L951" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
load_authorization_code(self, client: OAuthClientInformationFull, authorization_code: str) -> AuthorizationCode | None
@ -315,7 +316,7 @@ Look up our client code and return authorization code object
with PKCE challenge for validation.
#### `exchange_authorization_code` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L1037" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `exchange_authorization_code` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L994" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
exchange_authorization_code(self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode) -> OAuthToken
@ -333,7 +334,7 @@ Implements the token factory pattern:
PKCE validation is handled by the MCP framework before this method is called.
#### `load_refresh_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L1207" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `load_refresh_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L1159" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None
@ -342,7 +343,7 @@ load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str)
Load refresh token from local storage.
#### `exchange_refresh_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L1215" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `exchange_refresh_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L1167" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
exchange_refresh_token(self, client: OAuthClientInformationFull, refresh_token: RefreshToken, scopes: list[str]) -> OAuthToken
@ -359,7 +360,7 @@ Implements two-tier refresh:
6. Keep same FastMCP refresh token (unless upstream rotates)
#### `load_access_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L1429" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `load_access_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L1368" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
load_access_token(self, token: str) -> AccessToken | None
@ -378,7 +379,7 @@ The FastMCP JWT is a reference token - all authorization data comes
from validating the upstream token via the TokenVerifier.
#### `revoke_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L1493" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `revoke_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L1424" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
revoke_token(self, token: AccessToken | RefreshToken) -> None
@ -390,16 +391,17 @@ Removes tokens from local storage and attempts to revoke them with
the upstream server if a revocation endpoint is configured.
#### `get_routes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L1537" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_routes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L1468" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_routes(self, mcp_path: str | None = None) -> list[Route]
```
Get OAuth routes with custom proxy token handler.
Get OAuth routes with custom handlers for better error UX.
This method creates standard OAuth routes and replaces the token endpoint
with our proxy handler that forwards requests to the upstream OAuth server.
This method creates standard OAuth routes and replaces:
- /authorize endpoint: Enhanced error responses for unregistered clients
- /token endpoint: OAuth 2.1 compliant error codes
**Args:**
- `mcp_path`: The path where the MCP endpoint is mounted (e.g., "/mcp")

View file

@ -52,7 +52,7 @@ that is OIDC compliant.
**Methods:**
#### `get_oidc_configuration` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oidc_proxy.py#L318" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_oidc_configuration` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oidc_proxy.py#L326" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oidc_proxy.py#L335" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_token_verifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oidc_proxy.py#L343" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_token_verifier(self) -> TokenVerifier

View file

@ -37,7 +37,7 @@ Example:
Settings for Auth0 OIDC provider.
### `Auth0Provider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/auth0.py#L62" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `Auth0Provider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/auth0.py#L63" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
An Auth0 provider implementation for FastMCP.

View file

@ -37,7 +37,7 @@ Example:
Settings for AWS Cognito OAuth provider.
### `AWSCognitoTokenVerifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/aws.py#L67" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `AWSCognitoTokenVerifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/aws.py#L68" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Token verifier that filters claims to Cognito-specific subset.
@ -45,7 +45,7 @@ Token verifier that filters claims to Cognito-specific subset.
**Methods:**
#### `verify_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/aws.py#L70" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `verify_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/aws.py#L71" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
verify_token(self, token: str) -> AccessToken | None
@ -54,7 +54,7 @@ verify_token(self, token: str) -> AccessToken | None
Verify token and filter claims to Cognito-specific subset.
### `AWSCognitoProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/aws.py#L94" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `AWSCognitoProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/aws.py#L95" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Complete AWS Cognito OAuth provider for FastMCP.
@ -72,7 +72,7 @@ Features:
**Methods:**
#### `get_token_verifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/aws.py#L232" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_token_verifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/aws.py#L241" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_token_verifier(self) -> TokenVerifier

View file

@ -20,7 +20,7 @@ using the OAuth Proxy pattern for non-DCR OAuth flows.
Settings for Azure OAuth provider.
### `AzureProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L60" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `AzureProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L61" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Azure (Microsoft Entra) OAuth provider for FastMCP.
@ -29,23 +29,33 @@ This provider implements Azure/Microsoft Entra ID authentication using the
OAuth Proxy pattern. It supports both organizational accounts and personal
Microsoft accounts depending on the tenant configuration.
Scope Handling:
- required_scopes: Provide unprefixed scope names (e.g., ["read", "write"])
→ Automatically prefixed with identifier_uri during initialization
→ Validated on all tokens and advertised to MCP clients
- additional_authorize_scopes: Provide full format (e.g., ["User.Read"])
→ NOT prefixed, NOT validated, NOT advertised to clients
→ Used to request Microsoft Graph or other upstream API permissions
Features:
- OAuth proxy to Azure/Microsoft identity platform
- JWT validation using tenant issuer and JWKS
- Supports tenant configurations: specific tenant ID, "organizations", or "consumers"
- Custom API scopes and Microsoft Graph scopes in a single provider
Setup:
1. Create an App registration in Azure Portal
2. Configure Web platform redirect URI: http://localhost:8000/auth/callback (or your custom path)
3. Add an Application ID URI. Either use the default (api://{client_id}) or set a custom one.
4. Add a custom scope.
5. Create a client secret.
6. Get Application (client) ID, Directory (tenant) ID, and client secret
3. Add an Application ID URI under "Expose an API" (defaults to api://{client_id})
4. Add custom scopes (e.g., "read", "write") under "Expose an API"
5. Set access token version to 2 in the App manifest: "requestedAccessTokenVersion": 2
6. Create a client secret
7. Get Application (client) ID, Directory (tenant) ID, and client secret
**Methods:**
#### `authorize` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L234" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `authorize` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L270" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str

View file

@ -35,7 +35,7 @@ Example:
Settings for GitHub OAuth provider.
### `GitHubTokenVerifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/github.py#L64" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `GitHubTokenVerifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/github.py#L65" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Token verifier for GitHub OAuth tokens.
@ -46,7 +46,7 @@ by calling GitHub's API to check if they're valid and get user info.
**Methods:**
#### `verify_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/github.py#L86" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `verify_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/github.py#L87" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
verify_token(self, token: str) -> AccessToken | None
@ -55,7 +55,7 @@ verify_token(self, token: str) -> AccessToken | None
Verify GitHub OAuth token by calling GitHub API.
### `GitHubProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/github.py#L169" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `GitHubProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/github.py#L170" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Complete GitHub OAuth provider for FastMCP.

View file

@ -35,7 +35,7 @@ Example:
Settings for Google OAuth provider.
### `GoogleTokenVerifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/google.py#L66" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `GoogleTokenVerifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/google.py#L67" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Token verifier for Google OAuth tokens.
@ -46,7 +46,7 @@ by calling Google's tokeninfo API to check if they're valid and get user info.
**Methods:**
#### `verify_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/google.py#L88" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `verify_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/google.py#L89" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
verify_token(self, token: str) -> AccessToken | None
@ -55,7 +55,7 @@ verify_token(self, token: str) -> AccessToken | None
Verify Google OAuth token by calling Google's tokeninfo API.
### `GoogleProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/google.py#L185" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `GoogleProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/google.py#L186" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Complete Google OAuth provider for FastMCP.

View file

@ -24,7 +24,7 @@ Choose based on your WorkOS setup and authentication requirements.
Settings for WorkOS OAuth provider.
### `WorkOSTokenVerifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L56" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `WorkOSTokenVerifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L57" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Token verifier for WorkOS OAuth tokens.
@ -35,7 +35,7 @@ the /oauth2/userinfo endpoint to check validity and get user info.
**Methods:**
#### `verify_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L81" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `verify_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L82" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
verify_token(self, token: str) -> AccessToken | None
@ -44,7 +44,7 @@ verify_token(self, token: str) -> AccessToken | None
Verify WorkOS OAuth token by calling userinfo endpoint.
### `WorkOSProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L128" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `WorkOSProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L129" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Complete WorkOS OAuth provider for FastMCP.
@ -65,9 +65,9 @@ Setup Requirements:
4. Note your Client ID and Client Secret
### `AuthKitProviderSettings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L274" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `AuthKitProviderSettings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L283" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `AuthKitProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L291" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `AuthKitProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L300" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
AuthKit metadata provider for DCR (Dynamic Client Registration).
@ -93,7 +93,7 @@ https://workos.com/docs/authkit/mcp/integrating/token-verification
**Methods:**
#### `get_routes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L374" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_routes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L383" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_routes(self, mcp_path: str | None = None) -> list[Route]

View file

@ -7,7 +7,7 @@ sidebarTitle: context
## Functions
### `set_context` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L91" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `set_context` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L94" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
set_context(context: Context) -> Generator[Context, None, None]
@ -15,7 +15,7 @@ set_context(context: Context) -> Generator[Context, None, None]
## Classes
### `LogData` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L67" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `LogData` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L70" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Data object for passing log arguments to client-side handlers.
@ -24,7 +24,7 @@ This provides an interface to match the Python standard library logging,
for compatibility with structured logging.
### `Context` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L100" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `Context` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L103" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Context object providing access to MCP capabilities.
@ -72,7 +72,7 @@ The context is optional - tools that don't need it can omit the parameter.
**Methods:**
#### `fastmcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L152" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `fastmcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L155" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
fastmcp(self) -> FastMCP
@ -81,7 +81,7 @@ fastmcp(self) -> FastMCP
Get the FastMCP instance.
#### `request_context` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L181" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `request_context` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L184" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
request_context(self) -> RequestContext[ServerSession, Any, Request]
@ -92,7 +92,7 @@ Access to the underlying request context.
If called outside of a request context, this will raise a ValueError.
#### `report_progress` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L191" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `report_progress` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L194" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
report_progress(self, progress: float, total: float | None = None, message: str | None = None) -> None
@ -105,7 +105,47 @@ Report progress for the current operation.
- `total`: Optional total value e.g. 100
#### `read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L218" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `list_resources` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L221" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_resources(self) -> list[MCPResource]
```
List all available resources from the server.
**Returns:**
- List of Resource objects available on the server
#### `list_prompts` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L229" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_prompts(self) -> list[MCPPrompt]
```
List all available prompts from the server.
**Returns:**
- List of Prompt objects available on the server
#### `get_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L237" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> GetPromptResult
```
Get a prompt by name with optional arguments.
**Args:**
- `name`: The name of the prompt to get
- `arguments`: Optional arguments to pass to the prompt
**Returns:**
- The prompt result
#### `read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L251" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
read_resource(self, uri: str | AnyUrl) -> list[ReadResourceContents]
@ -120,7 +160,7 @@ Read a resource by URI.
- The resource content as either text or bytes
#### `log` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L231" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `log` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L262" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
log(self, message: str, level: LoggingLevel | None = None, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None
@ -138,7 +178,7 @@ Messages sent to Clients are also logged to the `fastmcp.server.context.to_clien
- `extra`: Optional mapping for additional arguments
#### `client_id` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L260" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `client_id` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L291" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
client_id(self) -> str | None
@ -147,7 +187,7 @@ client_id(self) -> str | None
Get the client ID if available.
#### `request_id` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L269" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `request_id` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L300" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
request_id(self) -> str
@ -156,7 +196,7 @@ request_id(self) -> str
Get the unique ID for this request.
#### `session_id` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L274" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `session_id` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L305" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
session_id(self) -> str
@ -173,7 +213,7 @@ the same client session.
- for other transports.
#### `session` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L318" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `session` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L349" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
session(self) -> ServerSession
@ -182,7 +222,7 @@ session(self) -> ServerSession
Access to the underlying session for advanced usage.
#### `debug` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L323" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `debug` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L354" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
debug(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None
@ -193,7 +233,7 @@ Send a `DEBUG`-level message to the connected MCP Client.
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.
#### `info` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L339" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `info` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L370" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
info(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None
@ -204,7 +244,7 @@ Send a `INFO`-level message to the connected MCP Client.
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.
#### `warning` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L355" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `warning` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L386" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
warning(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None
@ -215,7 +255,7 @@ Send a `WARNING`-level message to the connected MCP Client.
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.
#### `error` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L371" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `error` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L402" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
error(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None
@ -226,7 +266,7 @@ Send a `ERROR`-level message to the connected MCP Client.
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.
#### `list_roots` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L387" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `list_roots` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L418" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_roots(self) -> list[Root]
@ -235,7 +275,7 @@ list_roots(self) -> list[Root]
List the roots available to the server, as indicated by the client.
#### `send_tool_list_changed` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L392" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `send_tool_list_changed` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L423" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
send_tool_list_changed(self) -> None
@ -244,7 +284,7 @@ send_tool_list_changed(self) -> None
Send a tool list changed notification to the client.
#### `send_resource_list_changed` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L396" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `send_resource_list_changed` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L427" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
send_resource_list_changed(self) -> None
@ -253,7 +293,7 @@ send_resource_list_changed(self) -> None
Send a resource list changed notification to the client.
#### `send_prompt_list_changed` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L400" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `send_prompt_list_changed` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L431" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
send_prompt_list_changed(self) -> None
@ -262,7 +302,7 @@ send_prompt_list_changed(self) -> None
Send a prompt list changed notification to the client.
#### `sample` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L404" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `sample` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L435" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
sample(self, messages: str | Sequence[str | SamplingMessage], system_prompt: str | None = None, include_context: IncludeContext | None = None, temperature: float | None = None, max_tokens: int | None = None, model_preferences: ModelPreferences | str | list[str] | None = None) -> TextContent | ImageContent | AudioContent
@ -275,25 +315,25 @@ completion from the client. The client must be appropriately configured,
or the request will error.
#### `elicit` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L488" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `elicit` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L519" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
elicit(self, message: str, response_type: None) -> AcceptedElicitation[dict[str, Any]] | DeclinedElicitation | CancelledElicitation
```
#### `elicit` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L500" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `elicit` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L531" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
elicit(self, message: str, response_type: type[T]) -> AcceptedElicitation[T] | DeclinedElicitation | CancelledElicitation
```
#### `elicit` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L510" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `elicit` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L541" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
elicit(self, message: str, response_type: list[str]) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation
```
#### `elicit` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L519" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `elicit` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L550" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
elicit(self, message: str, response_type: type[T] | list[str] | None = None) -> AcceptedElicitation[T] | AcceptedElicitation[dict[str, Any]] | AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation
@ -322,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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L612" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_http_request` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L643" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_http_request(self) -> Request
@ -331,7 +371,7 @@ get_http_request(self) -> Request
Get the active starlette request.
#### `set_state` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L627" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `set_state` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L658" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
set_state(self, key: str, value: Any) -> None
@ -340,7 +380,7 @@ set_state(self, key: str, value: Any) -> None
Set a value in the context state.
#### `get_state` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L631" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_state` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L662" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_state(self, key: str) -> Any

View file

@ -0,0 +1,91 @@
---
title: tool_injection
sidebarTitle: tool_injection
---
# `fastmcp.server.middleware.tool_injection`
A middleware for injecting tools into the MCP server context.
## Functions
### `list_prompts` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/tool_injection.py#L54" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_prompts(context: Context) -> list[Prompt]
```
List prompts available on the server.
### `get_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/tool_injection.py#L64" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_prompt(context: Context, name: Annotated[str, 'The name of the prompt to render.'], arguments: Annotated[dict[str, Any] | None, 'The arguments to pass to the prompt.'] = None) -> mcp.types.GetPromptResult
```
Render a prompt available on the server.
### `list_resources` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/tool_injection.py#L88" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_resources(context: Context) -> list[mcp.types.Resource]
```
List resources available on the server.
### `read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/tool_injection.py#L98" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
read_resource(context: Context, uri: Annotated[AnyUrl | str, 'The URI of the resource to read.']) -> list[ReadResourceContents]
```
Read a resource available on the server.
## Classes
### `ToolInjectionMiddleware` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/tool_injection.py#L21" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
A middleware for injecting tools into the context.
**Methods:**
#### `on_list_tools` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/tool_injection.py#L32" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
on_list_tools(self, context: MiddlewareContext[mcp.types.ListToolsRequest], call_next: CallNext[mcp.types.ListToolsRequest, Sequence[Tool]]) -> Sequence[Tool]
```
Inject tools into the response.
#### `on_call_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/tool_injection.py#L41" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
on_call_tool(self, context: MiddlewareContext[mcp.types.CallToolRequestParams], call_next: CallNext[mcp.types.CallToolRequestParams, ToolResult]) -> ToolResult
```
Intercept tool calls to injected tools.
### `PromptToolMiddleware` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/tool_injection.py#L80" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
A middleware for injecting prompts as tools into the context.
### `ResourceToolMiddleware` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/tool_injection.py#L111" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
A middleware for injecting resources as tools into the context.

View file

@ -10,7 +10,7 @@ FastMCP - A more ergonomic interface for MCP servers.
## Functions
### `default_lifespan` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L98" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `default_lifespan` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L108" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2684" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `add_resource_prefix` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2705" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2744" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `remove_resource_prefix` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2765" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2811" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `has_resource_prefix` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2832" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
has_resource_prefix(uri: str, prefix: str, prefix_format: Literal['protocol', 'path'] | None = None) -> bool
@ -143,53 +143,53 @@ False
## Classes
### `FastMCP` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L134" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `FastMCP` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L144" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
**Methods:**
#### `settings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L323" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `settings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L344" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
settings(self) -> Settings
```
#### `name` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L334" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `name` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L355" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
name(self) -> str
```
#### `instructions` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L338" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `instructions` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L359" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
instructions(self) -> str | None
```
#### `instructions` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L342" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `instructions` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L363" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
instructions(self, value: str | None) -> None
```
#### `version` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L346" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `version` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L367" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
version(self) -> str | None
```
#### `website_url` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L350" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `website_url` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L371" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
website_url(self) -> str | None
```
#### `icons` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L354" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `icons` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L375" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
icons(self) -> list[mcp.types.Icon]
```
#### `run_async` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L381" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `run_async` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L402" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L411" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `run` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L432" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L455" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_middleware` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L476" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_middleware(self, middleware: Middleware) -> None
```
#### `get_tools` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L458" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_tools` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L479" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L478" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L499" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_tool(self, key: str) -> Tool
```
#### `get_resources` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L484" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_resources` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L505" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L517" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L538" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_resource(self, key: str) -> Resource
```
#### `get_resource_templates` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L523" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_resource_templates` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L544" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L556" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_resource_template` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L577" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L563" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_prompts` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L584" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L583" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L604" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_prompt(self, key: str) -> Prompt
```
#### `custom_route` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L589" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `custom_route` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L610" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1286" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1307" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1311" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `remove_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1332" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1331" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_tool_transformation` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1352" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1337" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `remove_tool_transformation` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1358" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1342" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1363" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
tool(self, name_or_fn: AnyFunction) -> FunctionTool
```
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1359" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1380" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
tool(self, name_or_fn: str | None = None) -> Callable[[AnyFunction], FunctionTool]
```
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1375" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1396" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1509" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1530" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1531" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_template` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1552" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1553" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_resource_fn` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1574" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1591" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1612" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
resource(self, uri: str) -> Callable[[AnyFunction], Resource | ResourceTemplate]
@ -532,7 +532,7 @@ async def get_weather(city: str) -> str:
```
#### `add_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1731" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1752" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1754" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1775" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
prompt(self, name_or_fn: AnyFunction) -> FunctionPrompt
```
#### `prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1768" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1789" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
prompt(self, name_or_fn: str | None = None) -> Callable[[AnyFunction], FunctionPrompt]
```
#### `prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1781" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1802" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1925" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `run_stdio_async` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1946" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1955" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `run_http_async` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1976" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2034" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `run_sse_async` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2055" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2062" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `sse_app` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2083" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2093" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `streamable_http_app` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2114" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2114" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `http_app` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2135" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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,13 +723,13 @@ Create a Starlette app using the specified HTTP transport.
- A Starlette application configured with the specified transport
#### `run_streamable_http_async` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2163" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `run_streamable_http_async` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2184" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
run_streamable_http_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
```
#### `mount` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2188" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `mount` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2209" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
mount(self, server: FastMCP[LifespanResultT], prefix: str | None = None, as_proxy: bool | None = None) -> None
@ -783,7 +783,7 @@ automatically determined based on whether the server has a custom lifespan
- `prompt_separator`: Deprecated. Separator character for prompt names.
#### `import_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2307" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `import_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2328" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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
@ -824,7 +824,7 @@ applied using the protocol\://prefix/path format
- `prompt_separator`: Deprecated. Separator for prompt names.
#### `from_openapi` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2445" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `from_openapi` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2466" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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
@ -833,7 +833,7 @@ from_openapi(cls, openapi_spec: dict[str, Any], client: httpx.AsyncClient, route
Create a FastMCP server from an OpenAPI specification.
#### `from_fastapi` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2494" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `from_fastapi` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2515" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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
@ -842,7 +842,7 @@ from_fastapi(cls, app: Any, name: str | None = None, route_maps: list[RouteMap]
Create a FastMCP server from a FastAPI application.
#### `as_proxy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2557" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `as_proxy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2578" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
as_proxy(cls, backend: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy
@ -856,7 +856,7 @@ instance or any value accepted as the `transport` argument of
`fastmcp.client.Client` constructor.
#### `from_client` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2616" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `from_client` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2637" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
from_client(cls, client: Client[ClientTransportT], **settings: Any) -> FastMCPProxy
@ -865,10 +865,10 @@ from_client(cls, client: Client[ClientTransportT], **settings: Any) -> FastMCPPr
Create a FastMCP proxy server from a FastMCP client.
#### `generate_name` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2668" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `generate_name` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2689" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
generate_name(cls, name: str | None = None) -> str
```
### `MountedServer` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2678" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `MountedServer` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2699" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

View file

@ -7,7 +7,7 @@ sidebarTitle: settings
## Classes
### `ExtendedEnvSettingsSource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L35" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `ExtendedEnvSettingsSource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L36" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
A special EnvSettingsSource that allows for multiple env var prefixes to be used.
@ -17,17 +17,17 @@ Raises a deprecation warning if the old `FASTMCP_SERVER_` prefix is used.
**Methods:**
#### `get_field_value` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L42" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_field_value` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L43" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]
```
### `ExtendedSettingsConfigDict` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L62" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `ExtendedSettingsConfigDict` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L63" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `ExperimentalSettings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L66" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `ExperimentalSettings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L67" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `Settings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L85" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `Settings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L86" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
FastMCP settings.
@ -35,7 +35,7 @@ FastMCP settings.
**Methods:**
#### `get_setting` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L97" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_setting` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L98" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_setting(self, attr: str) -> Any
@ -45,7 +45,7 @@ Get a setting. If the setting contains one or more `__`, it will be
treated as a nested setting.
#### `set_setting` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L110" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `set_setting` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L111" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
set_setting(self, attr: str, value: Any) -> None
@ -55,13 +55,13 @@ Set a setting. If the setting contains one or more `__`, it will be
treated as a nested setting.
#### `settings_customise_sources` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L124" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `settings_customise_sources` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L125" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
settings_customise_sources(cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource) -> tuple[PydanticBaseSettingsSource, ...]
```
#### `settings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L142" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `settings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L143" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
settings(self) -> Self
@ -71,13 +71,13 @@ This property is for backwards compatibility with FastMCP < 2.8.0,
which accessed fastmcp.settings.settings
#### `normalize_log_level` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L162" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `normalize_log_level` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L163" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
normalize_log_level(cls, v)
```
#### `server_auth_class` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L377" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `server_auth_class` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L378" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
server_auth_class(self) -> AuthProvider | None

View file

@ -15,7 +15,7 @@ Manages FastMCP tools.
**Methods:**
#### `has_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool_manager.py#L56" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `has_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool_manager.py#L60" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
has_tool(self, key: str) -> bool
@ -24,7 +24,7 @@ has_tool(self, key: str) -> bool
Check if a tool exists.
#### `get_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool_manager.py#L61" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool_manager.py#L65" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_tool(self, key: str) -> Tool
@ -33,7 +33,7 @@ get_tool(self, key: str) -> Tool
Get tool by key.
#### `get_tools` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool_manager.py#L68" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_tools` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool_manager.py#L72" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_tools(self) -> dict[str, Tool]
@ -42,7 +42,7 @@ get_tools(self) -> dict[str, Tool]
Gets the complete, unfiltered inventory of local tools.
#### `add_tool_from_fn` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool_manager.py#L74" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_tool_from_fn` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool_manager.py#L78" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_tool_from_fn(self, fn: Callable[..., Any], name: str | None = None, description: str | None = None, tags: set[str] | None = None, annotations: ToolAnnotations | None = None, serializer: Callable[[Any], str] | None = None, exclude_args: list[str] | None = None) -> Tool
@ -51,7 +51,7 @@ add_tool_from_fn(self, fn: Callable[..., Any], name: str | None = None, descript
Add a tool to the server.
#### `add_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool_manager.py#L103" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool_manager.py#L107" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_tool(self, tool: Tool) -> Tool
@ -60,7 +60,7 @@ add_tool(self, tool: Tool) -> Tool
Register a tool with the server.
#### `add_tool_transformation` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool_manager.py#L120" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_tool_transformation` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool_manager.py#L124" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_tool_transformation(self, tool_name: str, transformation: ToolTransformConfig) -> None
@ -69,7 +69,7 @@ add_tool_transformation(self, tool_name: str, transformation: ToolTransformConfi
Add a tool transformation.
#### `get_tool_transformation` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool_manager.py#L126" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_tool_transformation` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool_manager.py#L130" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_tool_transformation(self, tool_name: str) -> ToolTransformConfig | None
@ -78,7 +78,7 @@ get_tool_transformation(self, tool_name: str) -> ToolTransformConfig | None
Get a tool transformation.
#### `remove_tool_transformation` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool_manager.py#L130" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `remove_tool_transformation` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool_manager.py#L134" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
remove_tool_transformation(self, tool_name: str) -> None
@ -87,7 +87,7 @@ remove_tool_transformation(self, tool_name: str) -> None
Remove a tool transformation.
#### `remove_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool_manager.py#L135" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `remove_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool_manager.py#L139" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
remove_tool(self, key: str) -> None
@ -102,7 +102,7 @@ Remove a tool from the server.
- `NotFoundError`: If the tool is not found
#### `call_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool_manager.py#L149" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `call_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool_manager.py#L153" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
call_tool(self, key: str, arguments: dict[str, Any]) -> ToolResult

View file

@ -37,7 +37,7 @@ run, inspect, and dev commands.
- Tuple of (MCPServerConfig, resolved_server_spec)
### `log_server_banner` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/cli.py#L170" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `log_server_banner` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/cli.py#L200" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
log_server_banner(server: FastMCP[Any], transport: Literal['stdio', 'http', 'sse', 'streamable-http']) -> None

View file

@ -93,14 +93,22 @@ mcp = FastMCP(name="My Server", auth=auth)
HTTP request timeout in seconds for fetching OIDC configuration
</ParamField>
<ParamField body="token_verifier" type="TokenVerifier | None">
<VersionBadge version="2.13.1" />
Custom token verifier for validating tokens. When provided, FastMCP uses your custom verifier instead of creating a default `JWTVerifier`.
Cannot be used with `algorithm` or `required_scopes` parameters - configure these on your verifier instead. The verifier's `required_scopes` are automatically loaded and advertised.
</ParamField>
<ParamField body="algorithm" type="str | None">
JWT algorithm to use for token verification (e.g., "RS256"). If not specified,
uses the provider's default.
uses the provider's default. Only used when `token_verifier` is not provided.
</ParamField>
<ParamField body="required_scopes" type="list[str] | None">
List of OAuth scopes to request from the provider. These are automatically
included in authorization requests.
List of OAuth scopes for token validation. These are automatically
included in authorization requests. Only used when `token_verifier` is not provided.
</ParamField>
<ParamField body="redirect_path" type="str" default="/auth/callback">

View file

@ -210,6 +210,67 @@ Static token verification stores tokens as plain text and should never be used i
</Warning>
### Debug/Custom Token Verification
The `DebugTokenVerifier` provides maximum flexibility for testing and special cases where standard token verification isn't applicable. It delegates validation to a user-provided callable, making it useful for prototyping, testing scenarios, or handling opaque tokens without introspection endpoints.
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.debug import DebugTokenVerifier
# Accept all tokens (useful for rapid development)
verifier = DebugTokenVerifier()
mcp = FastMCP(name="Development Server", auth=verifier)
```
By default, `DebugTokenVerifier` accepts any non-empty token as valid. This eliminates authentication barriers during early development, allowing you to focus on core functionality before adding security.
For more controlled testing, provide custom validation logic:
```python
from fastmcp.server.auth.providers.debug import DebugTokenVerifier
# Synchronous validation - check token prefix
verifier = DebugTokenVerifier(
validate=lambda token: token.startswith("dev-"),
client_id="development-client",
scopes=["read", "write"]
)
mcp = FastMCP(name="Development Server", auth=verifier)
```
The validation callable can also be async, enabling database lookups or external service calls:
```python
from fastmcp.server.auth.providers.debug import DebugTokenVerifier
# Asynchronous validation - check against cache
async def validate_token(token: str) -> bool:
# Check if token exists in Redis, database, etc.
return await redis.exists(f"valid_tokens:{token}")
verifier = DebugTokenVerifier(
validate=validate_token,
client_id="api-client",
scopes=["api:access"]
)
mcp = FastMCP(name="Custom API", auth=verifier)
```
**Use Cases:**
- **Testing**: Accept any token during integration tests without setting up token infrastructure
- **Prototyping**: Quickly validate concepts without authentication complexity
- **Opaque tokens without introspection**: When you have tokens from an IDP that provides no introspection endpoint, and you're willing to accept tokens without validation (validation happens later at the upstream service)
- **Custom token formats**: Implement validation for non-standard token formats or legacy systems
<Warning>
`DebugTokenVerifier` bypasses standard security checks. Only use in controlled environments (development, testing) or when you fully understand the security implications. For production, use proper JWT or introspection-based verification.
</Warning>
### Test Token Generation
Test token generation helps when you need to test JWT verification without setting up complete identity infrastructure. FastMCP includes utilities for generating test key pairs and signed tokens.

View file

@ -115,6 +115,7 @@ For small icons or when you want to embed the icon directly, use data URIs:
```python
from mcp.types import Icon
from fastmcp.utilities.types import Image
# SVG icon as data URI
svg_icon = Icon(
@ -126,4 +127,13 @@ svg_icon = Icon(
def my_tool() -> str:
"""A tool with an embedded SVG icon."""
return "result"
# Generating a data URI from a local image file.
img = Image(path="./assets/brand/favicon.png")
icon = Icon(src=img.to_data_uri())
@mcp.tool(icons=[icon])
def file_icon_tool() -> str:
"""A tool with an icon generated from a local file."""
return "result"
```

View file

@ -143,6 +143,10 @@ The py-key-value-aio library includes additional implementations for various sto
For configuration details on these backends, consult the [py-key-value-aio documentation](https://github.com/strawgate/py-key-value).
<Warning>
Before using these backends in production, review the [py-key-value documentation](https://github.com/strawgate/py-key-value) to understand the maturity level and limitations of your chosen backend. Some backends may be in preview or have specific constraints that make them unsuitable for production use.
</Warning>
## Use Cases in FastMCP
### Server-Side OAuth Token Storage

View file

@ -103,7 +103,7 @@ from fastmcp import Client
async def main():
# Connect to the MCP server we just created
async with Client("http://127.0.0.1:8000/mcp/") as client:
async with Client("http://127.0.0.1:8000/mcp") as client:
# List the tools that were automatically generated
tools = await client.list_tools()

View file

@ -5,6 +5,24 @@ icon: "sparkles"
tag: NEW
---
<Update label="FastMCP 2.13.0" description="October 25, 2025" tags={["Releases"]}>
<Card
title="FastMCP 2.13.0: Cache Me If You Can"
href="https://github.com/jlowin/fastmcp/releases/tag/v2.13.0"
cta="Read the release notes"
>
FastMCP 2.13 "Cache Me If You Can" represents a fundamental maturation of the framework. After months of community feedback on authentication and state management, this release delivers the infrastructure FastMCP needs to handle production workloads: persistent storage, response caching, and pragmatic OAuth improvements that reflect real-world deployment challenges.
💾 **Pluggable storage backends** bring persistent state to FastMCP servers. Built on [py-key-value-aio](https://github.com/strawgate/py-key-value), a new library from FastMCP maintainer Bill Easton ([@strawgate](https://github.com/strawgate)), the storage layer provides encrypted disk storage by default, platform-aware token management, and a simple key-value interface for application state. We're excited to bring this elegantly designed library into the FastMCP ecosystem - it's both powerful and remarkably easy to use, including wrappers to add encryption, TTLs, caching, and more to backends ranging from Elasticsearch, Redis, DynamoDB, filesystem, in-memory, and more!
🔐 **OAuth maturity** brings months of production learnings into the framework. The new consent screen prevents confused deputy and authorization bypass attacks discovered in earlier versions, while the OAuth proxy now issues its own tokens with automatic key derivation. RFC 7662 token introspection support enables enterprise auth flows, and path prefix mounting enables OAuth-protected servers to integrate into existing web applications. FastMCP now supports out-of-the-box authentication with [WorkOS](https://gofastmcp.com/integrations/workos) and [AuthKit](https://gofastmcp.com/integrations/authkit), [GitHub](https://gofastmcp.com/integrations/github), [Google](https://gofastmcp.com/integrations/google), [Azure](https://gofastmcp.com/integrations/azure) (Entra ID), [AWS Cognito](https://gofastmcp.com/integrations/aws-cognito), [Auth0](https://gofastmcp.com/integrations/auth0), [Descope](https://gofastmcp.com/integrations/descope), [Scalekit](https://gofastmcp.com/integrations/scalekit), [JWTs](https://gofastmcp.com/servers/auth/token-verification#jwt-token-verification), and [RFC 7662 token introspection](https://gofastmcp.com/servers/auth/token-verification#token-introspection-protocol).
⚡ **Response Caching Middleware** dramatically improves performance for expensive operations, while **Server lifespans** provide proper initialization and cleanup hooks that run once per server instance instead of per client session.
✨ **Developer experience improvements** include Pydantic input validation, icon support, RFC 6570 query parameters for resource templates, improved Context API methods, and async file/directory resources.
</Card>
</Update>
<Update label="FastMCP 2.12.5" description="October 17, 2025" tags={["Releases"]}>
<Card
title="FastMCP 2.12.5: Safety Pin"

View file

@ -7,14 +7,14 @@ from ._read import fetch_notifications, fetch_timeline, search_for_posts
from ._social import follow_user_by_handle, like_post_by_uri, repost_by_uri
__all__ = [
"get_client",
"get_profile_info",
"create_post",
"create_thread",
"fetch_timeline",
"search_for_posts",
"fetch_notifications",
"fetch_timeline",
"follow_user_by_handle",
"get_client",
"get_profile_info",
"like_post_by_uri",
"repost_by_uri",
"search_for_posts",
]

View file

@ -7,6 +7,8 @@ Required environment variables:
- AZURE_CLIENT_SECRET: Your Azure client secret
- AZURE_TENANT_ID: Tenant ID
Options: "organizations" (work/school), "consumers" (personal), or specific tenant ID
- AZURE_REQUIRED_SCOPES: At least one scope required (e.g., "read" or "read,write")
These must match scope names created under "Expose an API" in your Azure App registration
To run:
python server.py
@ -18,11 +20,14 @@ from fastmcp import FastMCP
from fastmcp.server.auth.providers.azure import AzureProvider
auth = AzureProvider(
client_id=os.getenv("AZURE_CLIENT_ID") or "",
client_secret=os.getenv("AZURE_CLIENT_SECRET") or "",
tenant_id=os.getenv("AZURE_TENANT_ID")
client_id=os.getenv("FASTMCP_SERVER_AUTH_AZURE_CLIENT_ID") or "",
client_secret=os.getenv("FASTMCP_SERVER_AUTH_AZURE_CLIENT_SECRET") or "",
tenant_id=os.getenv("FASTMCP_SERVER_AUTH_AZURE_TENANT_ID")
or "", # Required for single-tenant apps - get from Azure Portal
base_url="http://localhost:8000",
required_scopes=["read"],
# required_scopes is automatically loaded from FASTMCP_SERVER_AUTH_AZURE_REQUIRED_SCOPES
# At least one scope is required - use unprefixed scope names from your Azure App (e.g., ["read", "write"])
# redirect_path="/auth/callback", # Default path - change if using a different callback URL
)

View file

@ -1,3 +1,9 @@
# /// script
# dependencies = ["aiohttp", "fastmcp"]
# ///
# uv pip install aiohttp fastmcp
import aiohttp
from fastmcp.server import FastMCP

View file

@ -19,7 +19,7 @@ from typing import Annotated, Any, Self
import asyncpg
import numpy as np
from openai import AsyncOpenAI
from pgvector.asyncpg import register_vector # Import register_vector
from pgvector.asyncpg import register_vector
from pydantic import BaseModel, Field
from pydantic_ai import Agent
@ -149,7 +149,9 @@ class MemoryNode(BaseModel):
)
self.importance += other.importance
self.access_count += other.access_count
self.embedding = [(a + b) / 2 for a, b in zip(self.embedding, other.embedding)]
self.embedding = [
(a + b) / 2 for a, b in zip(self.embedding, other.embedding, strict=True)
]
self.summary = await do_ai(
self.content, "Summarize the following text concisely.", str, deps
)
@ -281,9 +283,9 @@ async def display_memory_tree(deps: Deps) -> str:
@mcp.tool
async def remember(
contents: list[str] = Field(
description="List of observations or memories to store"
),
contents: Annotated[
list[str], Field(description="List of observations or memories to store")
],
):
deps = Deps(openai=AsyncOpenAI(), pool=await get_db_pool())
try:

View file

@ -12,12 +12,13 @@ dependencies = [
"platformdirs>=4.0.0",
"rich>=13.9.4",
"cyclopts>=3.0.0",
"authlib>=1.5.2",
"authlib>=1.6.5",
"pydantic[email]>=2.11.7",
"pyperclip>=1.9.0",
"openapi-core>=0.19.5",
"py-key-value-aio[disk,keyring,memory]>=0.2.6,<0.3.0",
"py-key-value-aio[disk,keyring,memory]>=0.2.8,<0.3.0",
"uvicorn>=0.35",
"websockets>=15.0.1",
"jsonschema-path>=0.3.4",
]
requires-python = ">=3.10"
@ -102,6 +103,11 @@ fallback-version = "0.0.0"
[tool.pytest.ini_options]
asyncio_mode = "auto"
# filterwarnings = ["error::DeprecationWarning"]
filterwarnings = [
# Suppress OAuth in-memory token storage warnings in tests
# Tests intentionally use ephemeral storage; this warning is for end users
"ignore:Using in-memory token storage:UserWarning",
]
timeout = 5
env = [
"FASTMCP_TEST_MODE=1",
@ -137,12 +143,34 @@ unknown-argument = "ignore" # 61 errors
call-non-callable = "ignore" # 7 errors
[tool.ruff.lint]
extend-select = ["I", "UP"]
fixable = ["ALL"]
ignore = [
"COM812",
"PLR0913", # Too many arguments, MCP Servers have a lot of arguments, OKAY?!
"SIM102", # Dont require combining if statements
]
extend-select = [
"B", # flake8-bugbear: Catches actual bugs like mutable default arguments
"C4", # flake8-comprehensions: More efficient/readable comprehensions
"I", # flake8-builtins: Catches builtins that are not explicitly imported
"PIE", # flake8-pie: More idiomatic Python code
"RUF", # Ruff-specific: Modern best practices unique to Ruff
"SIM", # flake8-simplify: Simplifies verbose code patterns
"UP", # flake8-unused-imports: Catches unused imports
]
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401", "I001", "RUF013"]
# allow imports not at the top of the file
"src/fastmcp/__init__.py" = ["E402"]
"!src/**.py" = [ # Only enforce extended ruff rules for code in src/
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"PIE", # flake8-pie
"RUF", # Ruff-specific
"SIM", # flake8-simplify
]
[tool.codespell]
ignore-words-list = "asend,shttp,te"

View file

@ -48,9 +48,9 @@ def __getattr__(name: str):
__all__ = [
"FastMCP",
"Context",
"client",
"Client",
"Context",
"FastMCP",
"client",
"settings",
]

View file

@ -78,7 +78,7 @@ def with_argv(args: list[str] | None):
original = sys.argv[:]
try:
# Preserve the script name (sys.argv[0]) and replace the rest
sys.argv = [sys.argv[0]] + args
sys.argv = [sys.argv[0], *args]
yield
finally:
sys.argv = original
@ -277,7 +277,7 @@ async def dev(
# Run the MCP Inspector command
process = subprocess.run(
[npx_cmd, inspector_cmd] + uv_cmd,
[npx_cmd, inspector_cmd, *uv_cmd],
check=True,
env=env,
)
@ -502,7 +502,7 @@ async def run(
process = subprocess.run(cmd, check=True, env=env)
sys.exit(process.returncode)
except subprocess.CalledProcessError as e:
logger.error(
logger.exception(
f"Failed to run: {e}",
extra={
"server_spec": server_spec,
@ -526,7 +526,7 @@ async def run(
skip_source=skip_source,
)
except Exception as e:
logger.error(
logger.exception(
f"Failed to run: {e}",
extra={
"server_spec": server_spec,
@ -766,13 +766,12 @@ async def inspect(
console.print(formatted_json.decode("utf-8"))
except Exception as e:
logger.error(
logger.exception(
f"Failed to inspect server: {e}",
extra={
"server_spec": server_spec,
"error": str(e),
},
exc_info=True,
)
console.print(f"[bold red]✗[/bold red] Failed to inspect server: {e}")
sys.exit(1)

View file

@ -15,18 +15,18 @@ from .transports import (
from .auth import OAuth, BearerAuth
__all__ = [
"BearerAuth",
"Client",
"ClientTransport",
"WSTransport",
"FastMCPTransport",
"NodeStdioTransport",
"NpxStdioTransport",
"OAuth",
"PythonStdioTransport",
"SSETransport",
"StdioTransport",
"PythonStdioTransport",
"NodeStdioTransport",
"UvxStdioTransport",
"UvStdioTransport",
"NpxStdioTransport",
"FastMCPTransport",
"StreamableHttpTransport",
"OAuth",
"BearerAuth",
"UvStdioTransport",
"UvxStdioTransport",
"WSTransport",
]

View file

@ -36,8 +36,6 @@ logger = get_logger(__name__)
class ClientNotFoundError(Exception):
"""Raised when OAuth client credentials are not found on the server."""
pass
async def check_if_auth_required(
mcp_url: str, httpx_kwargs: dict[str, Any] | None = None
@ -58,7 +56,7 @@ async def check_if_auth_required(
return True
# Check for WWW-Authenticate header
if "WWW-Authenticate" in response.headers:
if "WWW-Authenticate" in response.headers: # noqa: SIM103
return True
# If we get a successful response, auth may not be required
@ -194,8 +192,10 @@ class OAuth(OAuthClientProvider):
from warnings import warn
warn(
message="Using in-memory token storage is not recommended for production use -- "
+ "tokens will be lost on server restart."
message="Using in-memory token storage -- tokens will be lost when the client restarts. "
+ "For persistent storage across multiple MCP servers, provide an encrypted AsyncKeyValue backend. "
+ "See https://gofastmcp.com/clients/auth/oauth#token-storage for details.",
stacklevel=2,
)
self.token_storage_adapter: TokenStorageAdapter = TokenStorageAdapter(
@ -272,8 +272,10 @@ class OAuth(OAuthClientProvider):
if result.error:
raise result.error
return result.code, result.state # type: ignore
except TimeoutError:
raise TimeoutError(f"OAuth callback timed out after {TIMEOUT} seconds")
except TimeoutError as e:
raise TimeoutError(
f"OAuth callback timed out after {TIMEOUT} seconds"
) from e
finally:
server.should_exit = True
await anyio.sleep(0.1) # Allow server to shut down gracefully

View file

@ -61,15 +61,15 @@ from .transports import (
__all__ = [
"Client",
"SessionKwargs",
"RootsHandler",
"RootsList",
"ClientSamplingHandler",
"ElicitationHandler",
"LogHandler",
"MessageHandler",
"ClientSamplingHandler",
"SamplingHandler",
"ElicitationHandler",
"ProgressHandler",
"RootsHandler",
"RootsList",
"SamplingHandler",
"SessionKwargs",
]
logger = get_logger(__name__)
@ -362,10 +362,10 @@ class Client(Generic[ClientTransportT]):
await self._session_state.session.initialize()
)
yield
except anyio.ClosedResourceError:
raise RuntimeError("Server session was closed unexpectedly")
except TimeoutError:
raise RuntimeError("Failed to initialize server session")
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

View file

@ -11,7 +11,7 @@ from mcp.types import SamplingMessage
from fastmcp.server.sampling.handler import ServerSamplingHandler
__all__ = ["SamplingMessage", "SamplingParams", "SamplingHandler"]
__all__ = ["SamplingHandler", "SamplingMessage", "SamplingParams"]
ClientSamplingHandler: TypeAlias = Callable[

View file

@ -46,16 +46,16 @@ ClientTransportT = TypeVar("ClientTransportT", bound="ClientTransport")
__all__ = [
"ClientTransport",
"SSETransport",
"StreamableHttpTransport",
"StdioTransport",
"PythonStdioTransport",
"FastMCPStdioTransport",
"NodeStdioTransport",
"UvxStdioTransport",
"UvStdioTransport",
"NpxStdioTransport",
"FastMCPTransport",
"NodeStdioTransport",
"NpxStdioTransport",
"PythonStdioTransport",
"SSETransport",
"StdioTransport",
"StreamableHttpTransport",
"UvStdioTransport",
"UvxStdioTransport",
"infer_transport",
]
@ -109,9 +109,8 @@ class ClientTransport(abc.ABC):
# Basic representation for subclasses
return f"<{self.__class__.__name__}>"
async def close(self):
async def close(self): # noqa: B027
"""Close the transport."""
pass
def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
if auth is not None:
@ -141,10 +140,10 @@ class WSTransport(ClientTransport):
) -> AsyncIterator[ClientSession]:
try:
from mcp.client.websocket import websocket_client
except ImportError:
except ImportError as e:
raise ImportError(
"The websocket transport is not available. Please install fastmcp[websockets] or install the websockets package manually."
)
) from e
async with websocket_client(self.url) as transport:
read_stream, write_stream = transport
@ -207,7 +206,7 @@ class SSETransport(ClientTransport):
# instead we simply leave the kwarg out if it's not provided
if self.sse_read_timeout is not None:
client_kwargs["sse_read_timeout"] = self.sse_read_timeout.total_seconds()
if session_kwargs.get("read_timeout_seconds", None) is not None:
if session_kwargs.get("read_timeout_seconds") is not None:
read_timeout_seconds = cast(
datetime.timedelta, session_kwargs.get("read_timeout_seconds")
)
@ -277,7 +276,7 @@ class StreamableHttpTransport(ClientTransport):
# instead we simply leave the kwarg out if it's not provided
if self.sse_read_timeout is not None:
client_kwargs["sse_read_timeout"] = self.sse_read_timeout
if session_kwargs.get("read_timeout_seconds", None) is not None:
if session_kwargs.get("read_timeout_seconds") is not None:
client_kwargs["timeout"] = session_kwargs.get("read_timeout_seconds")
if self.httpx_client_factory is not None:
@ -451,8 +450,7 @@ async def _stdio_transport_connect_task(
if log_file is None:
log_file_handle = sys.stderr
elif isinstance(log_file, Path):
log_file_handle = open(log_file, "a")
stack.callback(log_file_handle.close)
log_file_handle = stack.enter_context(log_file.open("a"))
else:
# Must be TextIO - use it directly
log_file_handle = log_file
@ -852,26 +850,28 @@ class FastMCPTransport(ClientTransport):
server_read, server_write = server_streams
# Create a cancel scope for the server task
async with anyio.create_task_group() as tg:
async with _enter_server_lifespan(server=self.server):
tg.start_soon(
lambda: self.server._mcp_server.run(
server_read,
server_write,
self.server._mcp_server.create_initialization_options(),
raise_exceptions=self.raise_exceptions,
)
async with (
anyio.create_task_group() as tg,
_enter_server_lifespan(server=self.server),
):
tg.start_soon(
lambda: self.server._mcp_server.run(
server_read,
server_write,
self.server._mcp_server.create_initialization_options(),
raise_exceptions=self.raise_exceptions,
)
)
try:
async with ClientSession(
read_stream=client_read,
write_stream=client_write,
**session_kwargs,
) as client_session:
yield client_session
finally:
tg.cancel_scope.cancel()
try:
async with ClientSession(
read_stream=client_read,
write_stream=client_write,
**session_kwargs,
) as client_session:
yield client_session
finally:
tg.cancel_scope.cancel()
def __repr__(self) -> str:
return f"<FastMCPTransport(server='{self.server.name}')>"
@ -952,7 +952,7 @@ class MCPConfigTransport(ClientTransport):
# if there's exactly one server, create a client for that server
elif len(self.config.mcpServers) == 1:
self.transport = list(self.config.mcpServers.values())[0].to_transport()
self.transport = next(iter(self.config.mcpServers.values())).to_transport()
self._underlying_transports.append(self.transport)
# otherwise create a composite client

View file

@ -1,4 +1,4 @@
from .component_manager import set_up_component_manager
from .component_service import ComponentService
__all__ = ["set_up_component_manager", "ComponentService"]
__all__ = ["ComponentService", "set_up_component_manager"]

View file

@ -97,11 +97,11 @@ def make_endpoint(action, component, config):
return JSONResponse(
{"message": f"{action.capitalize()}d {component}: {name}"}
)
except NotFoundError:
except NotFoundError as e:
raise StarletteHTTPException(
status_code=404,
detail=f"Unknown {component}: {name}",
)
) from e
return endpoint

View file

@ -2,7 +2,7 @@ from .mcp_mixin import MCPMixin, mcp_tool, mcp_resource, mcp_prompt
__all__ = [
"MCPMixin",
"mcp_tool",
"mcp_resource",
"mcp_prompt",
"mcp_resource",
"mcp_tool",
]

View file

@ -21,10 +21,10 @@ try:
ChatCompletionUserMessageParam,
)
from openai.types.shared.chat_model import ChatModel
except ImportError:
except ImportError as e:
raise ImportError(
"The `openai` package is not installed. Please install `fastmcp[openai]` or add `openai` to your dependencies manually."
)
) from e
from typing_extensions import override

View file

@ -22,17 +22,14 @@ from .components import (
# Export public symbols - maintaining backward compatibility
__all__ = [
# Server
"FastMCPOpenAPI",
# Routing
"MCPType",
"RouteMap",
"RouteMapFn",
"ComponentFn",
"DEFAULT_ROUTE_MAPPINGS",
"_determine_route_type",
# Components
"OpenAPITool",
"ComponentFn",
"FastMCPOpenAPI",
"MCPType",
"OpenAPIResource",
"OpenAPIResourceTemplate",
"OpenAPITool",
"RouteMap",
"RouteMapFn",
"_determine_route_type",
]

View file

@ -146,11 +146,11 @@ class OpenAPITool(Tool):
if e.response.text:
error_message += f" - {e.response.text}"
raise ValueError(error_message)
raise ValueError(error_message) from e
except httpx.RequestError as e:
# Handle request errors (connection, timeout, etc.)
raise ValueError(f"Request error: {str(e)}")
raise ValueError(f"Request error: {e!s}") from e
class OpenAPIResource(Resource):
@ -165,9 +165,11 @@ class OpenAPIResource(Resource):
name: str,
description: str,
mime_type: str = "application/json",
tags: set[str] = set(),
tags: set[str] | None = None,
timeout: float | None = None,
):
if tags is None:
tags = set()
super().__init__(
uri=AnyUrl(uri), # Convert string to AnyUrl
name=name,
@ -276,11 +278,11 @@ class OpenAPIResource(Resource):
if e.response.text:
error_message += f" - {e.response.text}"
raise ValueError(error_message)
raise ValueError(error_message) from e
except httpx.RequestError as e:
# Handle request errors (connection, timeout, etc.)
raise ValueError(f"Request error: {str(e)}")
raise ValueError(f"Request error: {e!s}") from e
class OpenAPIResourceTemplate(ResourceTemplate):
@ -295,9 +297,11 @@ class OpenAPIResourceTemplate(ResourceTemplate):
name: str,
description: str,
parameters: dict[str, Any],
tags: set[str] = set(),
tags: set[str] | None = None,
timeout: float | None = None,
):
if tags is None:
tags = set()
super().__init__(
uri_template=uri_template,
name=name,
@ -342,7 +346,7 @@ class OpenAPIResourceTemplate(ResourceTemplate):
# Export public symbols
__all__ = [
"OpenAPITool",
"OpenAPIResource",
"OpenAPIResourceTemplate",
"OpenAPITool",
]

View file

@ -121,10 +121,10 @@ def _determine_route_type(
# Export public symbols
__all__ = [
"DEFAULT_ROUTE_MAPPINGS",
"ComponentFn",
"MCPType",
"RouteMap",
"RouteMapFn",
"ComponentFn",
"DEFAULT_ROUTE_MAPPINGS",
"_determine_route_type",
]

View file

@ -40,29 +40,24 @@ from .json_schema_converter import (
# Export public symbols - maintaining backward compatibility
__all__ = [
# Models
"HTTPRoute",
"HttpMethod",
"JsonSchema",
"ParameterInfo",
"ParameterLocation",
"RequestBodyInfo",
"ResponseInfo",
"HttpMethod",
"ParameterLocation",
"JsonSchema",
# Parser
"parse_openapi_to_http_routes",
# Formatters
"_combine_schemas",
"_make_optional_parameter_nullable",
"clean_schema_for_display",
"convert_openapi_schema_to_json_schema",
"convert_schema_definitions",
"extract_output_schema_from_responses",
"format_array_parameter",
"format_deep_object_parameter",
"format_description_with_responses",
"format_json_for_description",
"format_simple_description",
"generate_example_from_schema",
# Schemas
"_combine_schemas",
"extract_output_schema_from_responses",
"clean_schema_for_display",
"_make_optional_parameter_nullable",
# JSON Schema Converter
"convert_openapi_schema_to_json_schema",
"convert_schema_definitions",
"parse_openapi_to_http_routes",
]

View file

@ -63,7 +63,7 @@ class RequestDirector:
# Step 4: Handle request body
if body is not None:
if isinstance(body, dict) or isinstance(body, list):
if isinstance(body, dict | list):
request_data["json"] = body
else:
request_data["content"] = body

View file

@ -164,10 +164,10 @@ def _convert_nullable_field(schema: dict[str, Any]) -> dict[str, Any]:
if isinstance(current_type, str):
result["type"] = [current_type, "null"]
elif isinstance(current_type, list) and "null" not in current_type:
result["type"] = current_type + ["null"]
result["type"] = [*current_type, "null"]
elif "oneOf" in result:
# Convert oneOf to anyOf with null
result["anyOf"] = result.pop("oneOf") + [{"type": "null"}]
result["anyOf"] = [*result.pop("oneOf"), {"type": "null"}]
elif "anyOf" in result:
# Add null to anyOf if not present
if not any(item.get("type") == "null" for item in result["anyOf"]):

View file

@ -79,10 +79,10 @@ class HTTPRoute(FastMCPBaseModel):
# Export public symbols
__all__ = [
"HTTPRoute",
"HttpMethod",
"JsonSchema",
"ParameterInfo",
"ParameterLocation",
"RequestBodyInfo",
"ResponseInfo",
"HttpMethod",
"ParameterLocation",
"JsonSchema",
]

View file

@ -178,7 +178,7 @@ class OpenAPIParser(
else:
# Special handling for components
if part == "components" and hasattr(target, "components"):
target = getattr(target, "components")
target = target.components
elif hasattr(target, part): # Fallback check
target = getattr(target, part, None)
else:
@ -554,9 +554,7 @@ class OpenAPIParser(
if "$ref" in obj and isinstance(obj["$ref"], str):
ref = obj["$ref"]
# Handle both converted and unconverted refs
if ref.startswith("#/$defs/"):
schema_name = ref.split("/")[-1]
elif ref.startswith("#/components/schemas/"):
if ref.startswith(("#/$defs/", "#/components/schemas/")):
schema_name = ref.split("/")[-1]
else:
return
@ -815,6 +813,6 @@ class OpenAPIParser(
# Export public symbols
__all__ = [
"parse_openapi_to_http_routes",
"OpenAPIParser",
"parse_openapi_to_http_routes",
]

View file

@ -585,9 +585,9 @@ def extract_output_schema_from_responses(
# Export public symbols
__all__ = [
"clean_schema_for_display",
"_combine_schemas",
"_combine_schemas_and_map_params",
"extract_output_schema_from_responses",
"_make_optional_parameter_nullable",
"clean_schema_for_display",
"extract_output_schema_from_responses",
]

View file

@ -288,9 +288,8 @@ class MCPConfig(BaseModel):
@classmethod
def from_file(cls, file_path: Path) -> Self:
"""Load configuration from JSON file."""
if file_path.exists():
if content := file_path.read_text().strip():
return cls.model_validate_json(content)
if file_path.exists() and (content := file_path.read_text().strip()):
return cls.model_validate_json(content)
raise ValueError(f"No MCP servers defined in the config: {file_path}")

View file

@ -2,8 +2,8 @@ from .prompt import Prompt, PromptMessage, Message
from .prompt_manager import PromptManager
__all__ = [
"Message",
"Prompt",
"PromptManager",
"PromptMessage",
"Message",
]

View file

@ -207,10 +207,7 @@ class FunctionPrompt(Prompt):
# Auto-detect context parameter if not provided
context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
if context_kwarg:
prune_params = [context_kwarg]
else:
prune_params = None
prune_params = [context_kwarg] if context_kwarg else None
parameters = compress_schema(parameters, prune_params=prune_params)
@ -290,10 +287,7 @@ class FunctionPrompt(Prompt):
if (
param.annotation == inspect.Parameter.empty
or param.annotation is str
):
converted_kwargs[param_name] = param_value
# If argument is not a string, pass as-is (already properly typed)
elif not isinstance(param_value, str):
) or not isinstance(param_value, str):
converted_kwargs[param_name] = param_value
else:
# Try to convert string argument using type adapter
@ -314,7 +308,7 @@ class FunctionPrompt(Prompt):
raise PromptError(
f"Could not convert argument '{param_name}' with value '{param_value}' "
f"to expected type {param.annotation}. Error: {e}"
)
) from e
else:
# Parameter not in function signature, pass as-is
converted_kwargs[param_name] = param_value
@ -376,10 +370,12 @@ class FunctionPrompt(Prompt):
content=TextContent(type="text", text=content),
)
)
except Exception:
raise PromptError("Could not convert prompt result to message.")
except Exception as e:
raise PromptError(
"Could not convert prompt result to message."
) from e
return messages
except Exception:
except Exception as e:
logger.exception(f"Error rendering prompt {self.name}")
raise PromptError(f"Error rendering prompt {self.name}.")
raise PromptError(f"Error rendering prompt {self.name}.") from e

View file

@ -10,13 +10,13 @@ from .types import (
from .resource_manager import ResourceManager
__all__ = [
"Resource",
"TextResource",
"BinaryResource",
"FunctionResource",
"FileResource",
"HttpResource",
"DirectoryResource",
"ResourceTemplate",
"FileResource",
"FunctionResource",
"HttpResource",
"Resource",
"ResourceManager",
"ResourceTemplate",
"TextResource",
]

View file

@ -217,9 +217,7 @@ class FunctionResource(Resource):
if isinstance(result, Resource):
return await result.read()
elif isinstance(result, bytes):
return result
elif isinstance(result, str):
elif isinstance(result, bytes | str):
return result
else:
return pydantic_core.to_json(result, fallback=str).decode()

View file

@ -235,8 +235,8 @@ class ResourceManager:
# Then check templates (local and mounted) only if not found in concrete resources
templates = await self.get_resource_templates()
for template_key in templates.keys():
if match_uri_template(uri_str, template_key):
for template_key in templates:
if match_uri_template(uri_str, template_key) is not None:
return True
return False
@ -262,7 +262,7 @@ class ResourceManager:
templates = await self.get_resource_templates()
for storage_key, template in templates.items():
# Try to match against the storage key (which might be a custom key)
if params := match_uri_template(uri_str, storage_key):
if (params := match_uri_template(uri_str, storage_key)) is not None:
try:
return await template.create_resource(
uri_str,
@ -318,7 +318,7 @@ class ResourceManager:
# 1b. Check local templates if not found in concrete resources
for key, template in self._templates.items():
if params := match_uri_template(uri_str, key):
if (params := match_uri_template(uri_str, key)) is not None:
try:
resource = await template.create_resource(uri_str, params=params)
return await resource.read()

View file

@ -5,11 +5,11 @@ from __future__ import annotations
import json
from pathlib import Path
import anyio
import anyio.to_thread
import httpx
import pydantic.json
from anyio import Path as AsyncPath
from pydantic import Field, ValidationInfo
from typing_extensions import override
from fastmcp.exceptions import ResourceError
from fastmcp.resources.resource import Resource
@ -54,6 +54,10 @@ class FileResource(Resource):
description="MIME type of the resource content",
)
@property
def _async_path(self) -> AsyncPath:
return AsyncPath(self.path)
@pydantic.field_validator("path")
@classmethod
def validate_absolute_path(cls, path: Path) -> Path:
@ -71,12 +75,13 @@ class FileResource(Resource):
mime_type = info.data.get("mime_type", "text/plain")
return not mime_type.startswith("text/")
@override
async def read(self) -> str | bytes:
"""Read the file content."""
try:
if self.is_binary:
return await anyio.to_thread.run_sync(self.path.read_bytes)
return await anyio.to_thread.run_sync(self.path.read_text)
return await self._async_path.read_bytes()
return await self._async_path.read_text()
except Exception as e:
raise ResourceError(f"Error reading file {self.path}") from e
@ -89,11 +94,12 @@ class HttpResource(Resource):
default="application/json", description="MIME type of the resource content"
)
@override
async def read(self) -> str | bytes:
"""Read the HTTP content."""
async with httpx.AsyncClient() as client:
response = await client.get(self.url)
response.raise_for_status()
_ = response.raise_for_status()
return response.text
@ -111,6 +117,10 @@ class DirectoryResource(Resource):
default="application/json", description="MIME type of the resource content"
)
@property
def _async_path(self) -> AsyncPath:
return AsyncPath(self.path)
@pydantic.field_validator("path")
@classmethod
def validate_absolute_path(cls, path: Path) -> Path:
@ -119,33 +129,29 @@ class DirectoryResource(Resource):
raise ValueError("Path must be absolute")
return path
def list_files(self) -> list[Path]:
async def list_files(self) -> list[Path]:
"""List files in the directory."""
if not self.path.exists():
if not await self._async_path.exists():
raise FileNotFoundError(f"Directory not found: {self.path}")
if not self.path.is_dir():
if not await self._async_path.is_dir():
raise NotADirectoryError(f"Not a directory: {self.path}")
try:
if self.pattern:
return (
list(self.path.glob(self.pattern))
if not self.recursive
else list(self.path.rglob(self.pattern))
)
return (
list(self.path.glob("*"))
if not self.recursive
else list(self.path.rglob("*"))
)
except Exception as e:
raise ResourceError(f"Error listing directory {self.path}: {e}")
pattern = self.pattern or "*"
glob_fn = self._async_path.rglob if self.recursive else self._async_path.glob
try:
return [Path(p) async for p in glob_fn(pattern) if await p.is_file()]
except Exception as e:
raise ResourceError(f"Error listing directory {self.path}") from e
@override
async def read(self) -> str: # Always returns JSON string
"""Read the directory listing."""
try:
files = await anyio.to_thread.run_sync(self.list_files)
file_list = [str(f.relative_to(self.path)) for f in files if f.is_file()]
files: list[Path] = await self.list_files()
file_list = [str(f.relative_to(self.path)) for f in files]
return json.dumps({"files": file_list}, indent=2)
except Exception:
raise ResourceError(f"Error reading directory {self.path}")
except Exception as e:
raise ResourceError(f"Error reading directory {self.path}") from e

View file

@ -3,4 +3,4 @@ from .context import Context
from . import dependencies
__all__ = ["FastMCP", "Context"]
__all__ = ["Context", "FastMCP"]

View file

@ -5,19 +5,23 @@ from .auth import (
AccessToken,
AuthProvider,
)
from .providers.debug import DebugTokenVerifier
from .providers.jwt import JWTVerifier, StaticTokenVerifier
from .oauth_proxy import OAuthProxy
from .oidc_proxy import OIDCProxy
__all__ = [
"AuthProvider",
"OAuthProvider",
"TokenVerifier",
"JWTVerifier",
"StaticTokenVerifier",
"RemoteAuthProvider",
"AccessToken",
"AuthProvider",
"DebugTokenVerifier",
"JWTVerifier",
"OAuthProvider",
"OAuthProxy",
"OIDCProxy",
"RemoteAuthProvider",
"StaticTokenVerifier",
"TokenVerifier",
]

View file

@ -23,7 +23,7 @@ from mcp.server.auth.settings import (
ClientRegistrationOptions,
RevocationOptions,
)
from pydantic import AnyHttpUrl
from pydantic import AnyHttpUrl, Field
from starlette.middleware import Middleware
from starlette.middleware.authentication import AuthenticationMiddleware
from starlette.routing import Route
@ -32,7 +32,7 @@ from starlette.routing import Route
class AccessToken(_SDKAccessToken):
"""AccessToken that includes all JWT claims."""
claims: dict[str, Any] = {}
claims: dict[str, Any] = Field(default_factory=dict)
class AuthProvider(TokenVerifierProtocol):

View file

@ -365,7 +365,19 @@ def create_consent_html(
)
# Need to allow form-action for form submission
csp_policy = "default-src 'none'; style-src 'unsafe-inline'; img-src https:; base-uri 'none'; form-action *"
# Chrome requires explicit scheme declarations in CSP form-action when redirect chains
# end in custom protocol schemes (e.g., cursor://). Parse redirect_uri to include its scheme.
parsed_redirect = urlparse(redirect_uri)
redirect_scheme = parsed_redirect.scheme.lower()
# Build form-action directive with standard schemes plus custom protocol if present
form_action_schemes = ["https:", "http:"]
if redirect_scheme and redirect_scheme not in ("http", "https"):
# Custom protocol scheme (e.g., cursor:, vscode:, etc.)
form_action_schemes.append(f"{redirect_scheme}:")
form_action_directive = " ".join(form_action_schemes)
csp_policy = f"default-src 'none'; style-src 'unsafe-inline'; img-src https:; base-uri 'none'; form-action {form_action_directive}"
return create_page(
content=content,
@ -375,6 +387,96 @@ def create_consent_html(
)
def 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
"""
import html as html_module
error_message_escaped = html_module.escape(error_message)
# Build error message box
error_box = f"""
<div class="info-box error">
<p>{error_message_escaped}</p>
</div>
"""
# Build error details section if provided
details_section = ""
if error_details:
detail_rows_html = "\n".join(
[
f"""
<div class="detail-row">
<div class="detail-label">{html_module.escape(label)}:</div>
<div class="detail-value">{html_module.escape(value)}</div>
</div>
"""
for label, value in error_details.items()
]
)
details_section = f"""
<details>
<summary>Error Details</summary>
<div class="detail-box">
{detail_rows_html}
</div>
</details>
"""
# Build the page content
content = f"""
<div class="container">
{create_logo(icon_url=server_icon_url, alt_text=server_name or "FastMCP")}
<h1>{html_module.escape(error_title)}</h1>
{error_box}
{details_section}
</div>
"""
# Additional styles needed for this page
# Override .info-box.error to use normal text color instead of red
additional_styles = (
INFO_BOX_STYLES
+ DETAILS_STYLES
+ DETAIL_BOX_STYLES
+ """
.info-box.error {
color: #111827;
}
"""
)
# Simple CSP policy for error pages (no forms needed)
csp_policy = (
"default-src 'none'; style-src 'unsafe-inline'; img-src https:; base-uri 'none'"
)
return create_page(
content=content,
title=error_title,
additional_styles=additional_styles,
csp_policy=csp_policy,
)
# -------------------------------------------------------------------------
# Handler Classes
# -------------------------------------------------------------------------
@ -1569,7 +1671,9 @@ class OAuthProxy(OAuthProvider):
# IdP Callback Forwarding
# -------------------------------------------------------------------------
async def _handle_idp_callback(self, request: Request) -> RedirectResponse:
async def _handle_idp_callback(
self, request: Request
) -> HTMLResponse | RedirectResponse:
"""Handle callback from upstream IdP and forward to client.
This implements the DCR-compliant callback forwarding:
@ -1584,32 +1688,37 @@ class OAuthProxy(OAuthProvider):
error = request.query_params.get("error")
if error:
error_description = request.query_params.get("error_description")
logger.error(
"IdP callback error: %s - %s",
error,
request.query_params.get("error_description"),
error_description,
)
# TODO: Forward error to client callback
return RedirectResponse(
url=f"data:text/html,<h1>OAuth Error</h1><p>{error}: {request.query_params.get('error_description', 'Unknown error')}</p>",
status_code=302,
# Show error page to user
html_content = create_error_html(
error_title="OAuth Error",
error_message=f"Authentication failed: {error_description or 'Unknown error'}",
error_details={"Error Code": error} if error else None,
)
return HTMLResponse(content=html_content, status_code=400)
if not idp_code or not txn_id:
logger.error("IdP callback missing code or transaction ID")
return RedirectResponse(
url="data:text/html,<h1>OAuth Error</h1><p>Missing authorization code or transaction ID</p>",
status_code=302,
html_content = create_error_html(
error_title="OAuth Error",
error_message="Missing authorization code or transaction ID from the identity provider.",
)
return HTMLResponse(content=html_content, status_code=400)
# Look up transaction data
transaction_model = await self._transaction_store.get(key=txn_id)
if not transaction_model:
logger.error("IdP callback with invalid transaction ID: %s", txn_id)
return RedirectResponse(
url="data:text/html,<h1>OAuth Error</h1><p>Invalid or expired transaction</p>",
status_code=302,
html_content = create_error_html(
error_title="OAuth Error",
error_message="Invalid or expired authorization transaction. Please try authenticating again.",
)
return HTMLResponse(content=html_content, status_code=400)
transaction = transaction_model.model_dump()
# Exchange IdP code for tokens (server-side)
@ -1663,11 +1772,11 @@ class OAuthProxy(OAuthProvider):
except Exception as e:
logger.error("IdP token exchange failed: %s", e)
# TODO: Forward error to client callback
return RedirectResponse(
url=f"data:text/html,<h1>OAuth Error</h1><p>Token exchange failed: {e}</p>",
status_code=302,
html_content = create_error_html(
error_title="OAuth Error",
error_message=f"Token exchange with identity provider failed: {e}",
)
return HTMLResponse(content=html_content, status_code=500)
# Generate our own authorization code for the client
client_code = secrets.token_urlsafe(32)
@ -1714,10 +1823,11 @@ class OAuthProxy(OAuthProvider):
except Exception as e:
logger.error("Error in IdP callback handler: %s", e, exc_info=True)
return RedirectResponse(
url="data:text/html,<h1>OAuth Error</h1><p>Internal server error during IdP callback</p>",
status_code=302,
html_content = create_error_html(
error_title="OAuth Error",
error_message="Internal server error during OAuth callback processing. Please try again.",
)
return HTMLResponse(content=html_content, status_code=500)
# -------------------------------------------------------------------------
# Consent Interstitial

View file

@ -123,10 +123,10 @@ class OIDCConfiguration(BaseModel):
try:
AnyHttpUrl(value)
except Exception:
except Exception as e:
message = f"Invalid URL for configuration metadata: {attr}"
logger.error(message)
raise ValueError(message)
raise ValueError(message) from e
enforce("issuer", True)
enforce("authorization_endpoint", True)
@ -206,6 +206,7 @@ class OIDCProxy(OAuthProxy):
audience: str | None = None,
timeout_seconds: int | None = None,
# Token verifier
token_verifier: TokenVerifier | None = None,
algorithm: str | None = None,
required_scopes: list[str] | None = None,
# FastMCP server configuration
@ -231,8 +232,11 @@ class OIDCProxy(OAuthProxy):
client_secret: Client secret for upstream server
audience: Audience for upstream server
timeout_seconds: HTTP request timeout in seconds
algorithm: Token verifier algorithm
required_scopes: Required OAuth scopes
token_verifier: Optional custom token verifier (e.g., IntrospectionTokenVerifier for opaque tokens).
If not provided, a JWTVerifier will be created using the OIDC configuration.
Cannot be used with algorithm or required_scopes parameters (configure these on your verifier instead).
algorithm: Token verifier algorithm (only used if token_verifier is not provided)
required_scopes: Required scopes for token validation (only used if token_verifier is not provided)
base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
to avoid 404s during discovery when mounting under a path.
@ -268,6 +272,19 @@ class OIDCProxy(OAuthProxy):
if not base_url:
raise ValueError("Missing required base URL")
# Validate that verifier-specific parameters are not used with custom verifier
if token_verifier is not None:
if algorithm is not None:
raise ValueError(
"Cannot specify 'algorithm' when providing a custom token_verifier. "
"Configure the algorithm on your token verifier instead."
)
if required_scopes is not None:
raise ValueError(
"Cannot specify 'required_scopes' when providing a custom token_verifier. "
"Configure required scopes on your token verifier instead."
)
if isinstance(config_url, str):
config_url = AnyHttpUrl(config_url)
@ -287,12 +304,14 @@ class OIDCProxy(OAuthProxy):
else None
)
token_verifier = self.get_token_verifier(
algorithm=algorithm,
audience=audience,
required_scopes=required_scopes,
timeout_seconds=timeout_seconds,
)
# Use custom verifier if provided, otherwise create default JWTVerifier
if token_verifier is None:
token_verifier = self.get_token_verifier(
algorithm=algorithm,
audience=audience,
required_scopes=required_scopes,
timeout_seconds=timeout_seconds,
)
init_kwargs = {
"upstream_authorization_endpoint": str(

View file

@ -6,7 +6,7 @@ using the OAuth Proxy pattern for non-DCR OAuth flows.
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any
from key_value.aio.protocols import AsyncKeyValue
from pydantic import SecretStr, field_validator
@ -46,6 +46,7 @@ class AzureProviderSettings(BaseSettings):
additional_authorize_scopes: list[str] | None = None
allowed_client_redirect_uris: list[str] | None = None
jwt_signing_key: str | None = None
base_authority: str = "login.microsoftonline.com"
@field_validator("required_scopes", mode="before")
@classmethod
@ -93,6 +94,7 @@ class AzureProvider(OAuthProxy):
from fastmcp import FastMCP
from fastmcp.server.auth.providers.azure import AzureProvider
# Standard Azure (Public Cloud)
auth = AzureProvider(
client_id="your-client-id",
client_secret="your-client-secret",
@ -103,6 +105,16 @@ class AzureProvider(OAuthProxy):
# identifier_uri defaults to api://{client_id}
)
# Azure Government
auth_gov = AzureProvider(
client_id="your-client-id",
client_secret="your-client-secret",
tenant_id="your-tenant-id",
required_scopes=["read", "write"],
base_authority="login.microsoftonline.us", # Override for Azure Gov
base_url="http://localhost:8000",
)
mcp = FastMCP("My App", auth=auth)
```
"""
@ -113,16 +125,17 @@ class AzureProvider(OAuthProxy):
client_id: str | NotSetT = NotSet,
client_secret: str | NotSetT = NotSet,
tenant_id: str | NotSetT = NotSet,
identifier_uri: str | None | NotSetT = NotSet,
identifier_uri: str | NotSetT | None = NotSet,
base_url: str | NotSetT = NotSet,
issuer_url: str | NotSetT = NotSet,
redirect_path: str | NotSetT = NotSet,
required_scopes: list[str] | None | NotSetT = NotSet,
additional_authorize_scopes: list[str] | None | NotSetT = NotSet,
required_scopes: list[str] | NotSetT | None = NotSet,
additional_authorize_scopes: list[str] | NotSetT | None = NotSet,
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
client_storage: AsyncKeyValue | None = None,
jwt_signing_key: str | bytes | NotSetT = NotSet,
require_authorization_consent: bool = True,
base_authority: str | NotSetT = NotSet,
) -> None:
"""Initialize Azure OAuth provider.
@ -138,6 +151,8 @@ class AzureProvider(OAuthProxy):
issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
to avoid 404s during discovery when mounting under a path.
redirect_path: Redirect path configured in Azure App registration (defaults to "/auth/callback")
base_authority: Azure authority base URL (defaults to "login.microsoftonline.com").
For Azure Government, use "login.microsoftonline.us".
required_scopes: Custom API scope names WITHOUT prefix (e.g., ["read", "write"]).
- Automatically prefixed with identifier_uri during initialization
- Validated on all tokens
@ -180,6 +195,7 @@ class AzureProvider(OAuthProxy):
"additional_authorize_scopes": additional_authorize_scopes,
"allowed_client_redirect_uris": allowed_client_redirect_uris,
"jwt_signing_key": jwt_signing_key,
"base_authority": base_authority,
}.items()
if v is not NotSet
}
@ -202,32 +218,35 @@ class AzureProvider(OAuthProxy):
)
raise ValueError(msg)
# Validate required_scopes has at least one scope
if not settings.required_scopes:
raise ValueError("required_scopes is required")
msg = (
"required_scopes must include at least one scope - set via parameter or "
"FASTMCP_SERVER_AUTH_AZURE_REQUIRED_SCOPES. Azure's OAuth API requires "
"the 'scope' parameter in authorization requests. Use the unprefixed scope "
"names from your Azure App registration (e.g., ['read', 'write'])"
)
raise ValueError(msg)
# Apply defaults
self.identifier_uri = settings.identifier_uri or f"api://{settings.client_id}"
self.additional_authorize_scopes = settings.additional_authorize_scopes or []
tenant_id_final = settings.tenant_id
# Prefix required scopes with identifier_uri for Azure
# Azure returns scopes as full URIs (e.g., "api://xxx/read") in tokens
prefixed_required_scopes = [
f"{self.identifier_uri}/{scope}" for scope in settings.required_scopes
]
# Always validate tokens against the app's API client ID using JWT
issuer = f"https://login.microsoftonline.com/{tenant_id_final}/v2.0"
base_authority_final = settings.base_authority
issuer = f"https://{base_authority_final}/{tenant_id_final}/v2.0"
jwks_uri = (
f"https://login.microsoftonline.com/{tenant_id_final}/discovery/v2.0/keys"
f"https://{base_authority_final}/{tenant_id_final}/discovery/v2.0/keys"
)
# Azure returns unprefixed scopes in JWT tokens, so validate against unprefixed scopes
token_verifier = JWTVerifier(
jwks_uri=jwks_uri,
issuer=issuer,
audience=settings.client_id,
algorithm="RS256",
required_scopes=prefixed_required_scopes,
required_scopes=settings.required_scopes, # Unprefixed scopes for validation
)
# Extract secret string from SecretStr
@ -237,10 +256,10 @@ class AzureProvider(OAuthProxy):
# Build Azure OAuth endpoints with tenant
authorization_endpoint = (
f"https://login.microsoftonline.com/{tenant_id_final}/oauth2/v2.0/authorize"
f"https://{base_authority_final}/{tenant_id_final}/oauth2/v2.0/authorize"
)
token_endpoint = (
f"https://login.microsoftonline.com/{tenant_id_final}/oauth2/v2.0/token"
f"https://{base_authority_final}/{tenant_id_final}/oauth2/v2.0/token"
)
# Initialize OAuth proxy with Azure endpoints
@ -260,11 +279,15 @@ class AzureProvider(OAuthProxy):
require_authorization_consent=require_authorization_consent,
)
authority_info = ""
if base_authority_final != "login.microsoftonline.com":
authority_info = f" using authority {base_authority_final}"
logger.info(
"Initialized Azure OAuth provider for client %s with tenant %s%s",
"Initialized Azure OAuth provider for client %s with tenant %s%s%s",
settings.client_id,
tenant_id_final,
f" and identifier_uri {self.identifier_uri}" if self.identifier_uri else "",
authority_info,
)
async def authorize(
@ -298,19 +321,40 @@ class AzureProvider(OAuthProxy):
"Filtering out 'resource' parameter '%s' for Azure AD v2.0 (use scopes instead)",
original_resource,
)
# Scopes are already prefixed:
# - self.required_scopes was prefixed during __init__
# - Client scopes come from PRM which advertises prefixed scopes
scopes = params_to_use.scopes or self.required_scopes
final_scopes = list(scopes)
# Add Microsoft Graph scopes separately - these use shorthand format (e.g., "User.Read")
# and should not be prefixed with identifier_uri. Azure returns them as-is in tokens.
if self.additional_authorize_scopes:
final_scopes.extend(self.additional_authorize_scopes)
modified_params = params_to_use.model_copy(update={"scopes": final_scopes})
auth_url = await super().authorize(client, modified_params)
# Don't modify the scopes in params - they stay unprefixed for MCP clients
# We'll prefix them when building the Azure authorization URL (in _build_upstream_authorize_url)
auth_url = await super().authorize(client, params_to_use)
separator = "&" if "?" in auth_url else "?"
return f"{auth_url}{separator}prompt=select_account"
def _build_upstream_authorize_url(
self, txn_id: str, transaction: dict[str, Any]
) -> str:
"""Build Azure authorization URL with prefixed scopes.
Overrides parent to prefix scopes with identifier_uri before sending to Azure,
while keeping unprefixed scopes in the transaction for MCP clients.
"""
# Get unprefixed scopes from transaction
unprefixed_scopes = transaction.get("scopes") or self.required_scopes or []
# Prefix scopes for Azure authorization request
prefixed_scopes = []
for scope in unprefixed_scopes:
if "://" in scope or "/" in scope:
# Already a full URI or path (e.g., "api://xxx/read" or "User.Read")
prefixed_scopes.append(scope)
else:
# Unprefixed scope name - prefix it with identifier_uri
prefixed_scopes.append(f"{self.identifier_uri}/{scope}")
# Add Microsoft Graph scopes (not validated, not prefixed)
if self.additional_authorize_scopes:
prefixed_scopes.extend(self.additional_authorize_scopes)
# Temporarily modify transaction dict for parent's URL building
modified_transaction = transaction.copy()
modified_transaction["scopes"] = prefixed_scopes
# Let parent build the URL with prefixed scopes
return super()._build_upstream_authorize_url(txn_id, modified_transaction)

View file

@ -11,7 +11,7 @@ from fastmcp.server.auth.providers.jwt import JWKData, JWKSData, RSAKeyPair
from fastmcp.server.auth.providers.jwt import JWTVerifier as BearerAuthProvider
# Re-export for backwards compatibility
__all__ = ["BearerAuthProvider", "RSAKeyPair", "JWKData", "JWKSData"]
__all__ = ["BearerAuthProvider", "JWKData", "JWKSData", "RSAKeyPair"]
# Deprecated in 2.11
if fastmcp.settings.deprecation_warnings:

View file

@ -0,0 +1,114 @@
"""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)
```
"""
from __future__ import annotations
import inspect
from collections.abc import Awaitable, Callable
from fastmcp.server.auth import TokenVerifier
from fastmcp.server.auth.auth import AccessToken
from fastmcp.utilities.logging import get_logger
logger = get_logger(__name__)
class DebugTokenVerifier(TokenVerifier):
"""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.
"""
def __init__(
self,
validate: Callable[[str], bool]
| Callable[[str], Awaitable[bool]] = lambda token: True,
client_id: str = "debug-client",
scopes: list[str] | None = None,
required_scopes: list[str] | None = None,
):
"""Initialize the debug token verifier.
Args:
validate: Callable that takes a token string and returns True if valid.
Can be sync or async. Default accepts all tokens.
client_id: Client ID to assign to validated tokens
scopes: Scopes to assign to validated tokens
required_scopes: Required scopes (inherited from TokenVerifier base class)
"""
super().__init__(required_scopes=required_scopes)
self.validate = validate
self.client_id = client_id
self.scopes = scopes or []
async def 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
"""
# Reject empty tokens
if not token or not token.strip():
logger.debug("Rejecting empty token")
return None
try:
# Call validation function and await if result is awaitable
result = self.validate(token)
if inspect.isawaitable(result):
is_valid = await result
else:
is_valid = result
if not is_valid:
logger.debug("Token validation failed: callable returned False")
return None
# Return valid AccessToken
return AccessToken(
token=token,
client_id=self.client_id,
scopes=self.scopes,
expires_at=None, # No expiration
claims={"token": token}, # Store original token in claims
)
except Exception as e:
logger.debug("Token validation error: %s", e, exc_info=True)
return None

View file

@ -96,10 +96,10 @@ class InMemoryOAuthProvider(OAuthProvider):
# or if params.redirect_uri is None and client has a default.
# However, the AuthorizationHandler handles the primary validation.
pass # Let's assume AuthorizationHandler did its job.
except Exception: # Replace with specific validation error if client.validate_redirect_uri existed
except Exception as e: # Replace with specific validation error if client.validate_redirect_uri existed
raise AuthorizeError(
error="invalid_request", error_description="Invalid redirect_uri."
)
) from e
auth_code_value = f"test_auth_code_{secrets.token_hex(16)}"
expires_at = time.time() + DEFAULT_AUTH_CODE_EXPIRY_SECONDS

View file

@ -97,8 +97,8 @@ class IntrospectionTokenVerifier(TokenVerifier):
client_id: str | NotSetT = NotSet,
client_secret: str | NotSetT = NotSet,
timeout_seconds: int | NotSetT = NotSet,
required_scopes: list[str] | None | NotSetT = NotSet,
base_url: AnyHttpUrl | str | None | NotSetT = NotSet,
required_scopes: list[str] | NotSetT | None = NotSet,
base_url: AnyHttpUrl | str | NotSetT | None = NotSet,
):
"""
Initialize the introspection token verifier.

View file

@ -150,7 +150,7 @@ class JWTVerifierSettings(BaseSettings):
public_key: str | None = None
jwks_uri: str | None = None
issuer: str | None = None
issuer: str | list[str] | None = None
algorithm: str | None = None
audience: str | list[str] | None = None
required_scopes: list[str] | None = None
@ -184,28 +184,28 @@ class JWTVerifier(TokenVerifier):
def __init__(
self,
*,
public_key: str | None | NotSetT = NotSet,
jwks_uri: str | None | NotSetT = NotSet,
issuer: str | None | NotSetT = NotSet,
audience: str | list[str] | None | NotSetT = NotSet,
algorithm: str | None | NotSetT = NotSet,
required_scopes: list[str] | None | NotSetT = NotSet,
base_url: AnyHttpUrl | str | None | NotSetT = NotSet,
public_key: str | NotSetT | None = NotSet,
jwks_uri: str | NotSetT | None = NotSet,
issuer: str | list[str] | NotSetT | None = NotSet,
audience: str | list[str] | NotSetT | None = NotSet,
algorithm: str | NotSetT | None = NotSet,
required_scopes: list[str] | NotSetT | None = NotSet,
base_url: AnyHttpUrl | str | NotSetT | None = NotSet,
):
"""
Initialize the JWT token verifier.
Initialize a JWTVerifier configured to validate JWTs using either a static key or a JWKS endpoint.
Args:
public_key: For asymmetric algorithms (RS256, ES256, etc.): PEM-encoded public key.
For symmetric algorithms (HS256, HS384, HS512): The shared secret string.
jwks_uri: URI to fetch JSON Web Key Set (only for asymmetric algorithms)
issuer: Expected issuer claim
audience: Expected audience claim(s)
algorithm: JWT signing algorithm. Supported algorithms:
- Asymmetric: RS256/384/512, ES256/384/512, PS256/384/512 (default: RS256)
- Symmetric: HS256, HS384, HS512
required_scopes: Required scopes for all tokens
base_url: Base URL for TokenVerifier protocol
Parameters:
public_key (str | NotSetT | None): PEM-encoded public key for asymmetric algorithms or shared secret for symmetric algorithms.
jwks_uri (str | NotSetT | None): URI to fetch a JSON Web Key Set; used when verifying tokens with remote JWKS.
issuer (str | list[str] | NotSetT | None): Expected issuer claim value or list of allowed issuer values.
audience (str | list[str] | NotSetT | None): Expected audience claim value or list of allowed audience values.
algorithm (str | NotSetT | None): JWT signing algorithm to accept (default: "RS256"). Supported: HS256/384/512, RS256/384/512, ES256/384/512, PS256/384/512.
required_scopes (list[str] | NotSetT | None): Scopes that must be present in validated tokens.
base_url (AnyHttpUrl | str | NotSetT | None): Base URL passed to the parent TokenVerifier.
Raises:
ValueError: If neither or both of `public_key` and `jwks_uri` are provided, or if `algorithm` is unsupported.
"""
settings = JWTVerifierSettings.model_validate(
{
@ -283,7 +283,7 @@ class JWTVerifier(TokenVerifier):
return await self._get_jwks_key(kid)
except Exception as e:
raise ValueError(f"Failed to extract key ID from token: {e}")
raise ValueError(f"Failed to extract key ID from token: {e}") from e
async def _get_jwks_key(self, kid: str | None) -> str:
"""Fetch key from JWKS with simple caching."""
@ -342,10 +342,10 @@ class JWTVerifier(TokenVerifier):
raise ValueError("No keys found in JWKS")
except httpx.HTTPError as e:
raise ValueError(f"Failed to fetch JWKS: {e}")
raise ValueError(f"Failed to fetch JWKS: {e}") from e
except Exception as e:
self.logger.debug(f"JWKS fetch failed: {e}")
raise ValueError(f"Failed to fetch JWKS: {e}")
raise ValueError(f"Failed to fetch JWKS: {e}") from e
def _extract_scopes(self, claims: dict[str, Any]) -> list[str]:
"""
@ -366,13 +366,13 @@ class JWTVerifier(TokenVerifier):
async def 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
Parameters:
token (str): 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.
"""
try:
# Get verification key (static or from JWKS)
@ -401,7 +401,18 @@ class JWTVerifier(TokenVerifier):
# Validate issuer - note we use issuer instead of issuer_url here because
# issuer is optional, allowing users to make this check optional
if self.issuer:
if claims.get("iss") != self.issuer:
iss = claims.get("iss")
# Handle different combinations of issuer types
issuer_valid = False
if isinstance(self.issuer, list):
# self.issuer is a list - check if token issuer matches any expected issuer
issuer_valid = iss in self.issuer
else:
# self.issuer is a string - check for equality
issuer_valid = iss == self.issuer
if not issuer_valid:
self.logger.debug(
"Token validation failed: issuer mismatch for client %s",
client_id,

View file

@ -83,7 +83,7 @@ class SupabaseProvider(RemoteAuthProvider):
*,
project_url: AnyHttpUrl | str | NotSetT = NotSet,
base_url: AnyHttpUrl | str | NotSetT = NotSet,
required_scopes: list[str] | None | NotSetT = NotSet,
required_scopes: list[str] | NotSetT | None = NotSet,
token_verifier: TokenVerifier | None = None,
):
"""Initialize Supabase metadata provider.

View file

@ -169,7 +169,7 @@ class WorkOSProvider(OAuthProxy):
base_url: AnyHttpUrl | str | NotSetT = NotSet,
issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
redirect_path: str | NotSetT = NotSet,
required_scopes: list[str] | None | NotSetT = NotSet,
required_scopes: list[str] | NotSetT | None = NotSet,
timeout_seconds: int | NotSetT = NotSet,
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
client_storage: AsyncKeyValue | None = None,
@ -338,7 +338,7 @@ class AuthKitProvider(RemoteAuthProvider):
*,
authkit_domain: AnyHttpUrl | str | NotSetT = NotSet,
base_url: AnyHttpUrl | str | NotSetT = NotSet,
required_scopes: list[str] | None | NotSetT = NotSet,
required_scopes: list[str] | NotSetT | None = NotSet,
token_verifier: TokenVerifier | None = None,
):
"""Initialize AuthKit metadata provider.

View file

@ -188,8 +188,8 @@ class Context:
"""
try:
return request_ctx.get()
except LookupError:
raise ValueError("Context is not available outside of a request")
except LookupError as e:
raise ValueError("Context is not available outside of a request") from e
async def report_progress(
self, progress: float, total: float | None = None, message: str | None = None
@ -224,8 +224,6 @@ class Context:
Returns:
List of Resource objects available on the server
"""
if self.fastmcp is None:
raise ValueError("Context is not available outside of a request")
return await self.fastmcp._list_resources_mcp()
async def list_prompts(self) -> list[MCPPrompt]:
@ -234,8 +232,6 @@ class Context:
Returns:
List of Prompt objects available on the server
"""
if self.fastmcp is None:
raise ValueError("Context is not available outside of a request")
return await self.fastmcp._list_prompts_mcp()
async def get_prompt(
@ -250,8 +246,6 @@ class Context:
Returns:
The prompt result
"""
if self.fastmcp is None:
raise ValueError("Context is not available outside of a request")
return await self.fastmcp._get_prompt_mcp(name, arguments)
async def read_resource(self, uri: str | AnyUrl) -> list[ReadResourceContents]:
@ -263,8 +257,6 @@ class Context:
Returns:
The resource content as either text or bytes
"""
if self.fastmcp is None:
raise ValueError("Context is not available outside of a request")
return await self.fastmcp._read_resource_mcp(uri)
async def log(
@ -350,7 +342,7 @@ class Context:
session_id = str(uuid4())
# Save the session id to the session attributes
setattr(session, "_fastmcp_id", session_id)
session._fastmcp_id = session_id
return session_id
@property
@ -603,13 +595,11 @@ class Context:
choice_literal = Literal[tuple(response_type)] # type: ignore
response_type = ScalarElicitationType[choice_literal] # type: ignore
# if the user provided a primitive scalar, wrap it in an object schema
elif response_type in {bool, int, float, str}:
response_type = ScalarElicitationType[response_type] # type: ignore
# if the user provided a Literal type, wrap it in an object schema
elif get_origin(response_type) is Literal:
response_type = ScalarElicitationType[response_type] # type: ignore
# if the user provided an Enum type, wrap it in an object schema
elif isinstance(response_type, type) and issubclass(response_type, Enum):
elif (
response_type in {bool, int, float, str}
or get_origin(response_type) is Literal
or (isinstance(response_type, type) and issubclass(response_type, Enum))
):
response_type = ScalarElicitationType[response_type] # type: ignore
response_type = cast(type[T], response_type)

View file

@ -1,5 +1,6 @@
from __future__ import annotations
import contextlib
from typing import TYPE_CHECKING
from mcp.server.auth.middleware.auth_context import (
@ -16,11 +17,11 @@ if TYPE_CHECKING:
from fastmcp.server.context import Context
__all__ = [
"get_context",
"get_http_request",
"get_http_headers",
"get_access_token",
"AccessToken",
"get_access_token",
"get_context",
"get_http_headers",
"get_http_request",
]
@ -43,10 +44,8 @@ def get_http_request() -> Request:
from mcp.server.lowlevel.server import request_ctx
request = None
try:
with contextlib.suppress(LookupError):
request = request_ctx.get().request
except LookupError:
pass
if request is None:
raise RuntimeError("No active HTTP request found.")

View file

@ -20,8 +20,8 @@ __all__ = [
"AcceptedElicitation",
"CancelledElicitation",
"DeclinedElicitation",
"get_elicitation_schema",
"ScalarElicitationType",
"get_elicitation_schema",
]
logger = get_logger(__name__)

View file

@ -342,9 +342,8 @@ def create_streamable_http_app(
# Create a lifespan manager to start and stop the session manager
@asynccontextmanager
async def lifespan(app: Starlette) -> AsyncGenerator[None, None]:
async with server._lifespan_manager():
async with session_manager.run():
yield
async with server._lifespan_manager(), session_manager.run():
yield
# Create and return the app with lifespan
app = create_base_app(

View file

@ -5,7 +5,7 @@ from .middleware import (
)
__all__ = [
"CallNext",
"Middleware",
"MiddlewareContext",
"CallNext",
]

View file

@ -46,7 +46,7 @@ class CachableReadResourceContents(BaseModel):
@classmethod
def get_sizes(cls, values: Sequence[Self]) -> int:
return sum([item.get_size() for item in values])
return sum(item.get_size() for item in values)
@classmethod
def wrap(cls, values: Sequence[ReadResourceContents]) -> list[Self]:

View file

@ -64,7 +64,7 @@ class ErrorHandlingMiddleware(Middleware):
error_key = f"{error_type}:{method}"
self.error_counts[error_key] = self.error_counts.get(error_key, 0) + 1
base_message = f"Error in {method}: {error_type}: {str(error)}"
base_message = f"Error in {method}: {error_type}: {error!s}"
if self.include_traceback:
self.logger.error(f"{base_message}\n{traceback.format_exc()}")
@ -91,24 +91,24 @@ class ErrorHandlingMiddleware(Middleware):
if error_type in (ValueError, TypeError):
return McpError(
ErrorData(code=-32602, message=f"Invalid params: {str(error)}")
ErrorData(code=-32602, message=f"Invalid params: {error!s}")
)
elif error_type in (FileNotFoundError, KeyError, NotFoundError):
return McpError(
ErrorData(code=-32001, message=f"Resource not found: {str(error)}")
ErrorData(code=-32001, message=f"Resource not found: {error!s}")
)
elif error_type is PermissionError:
return McpError(
ErrorData(code=-32000, message=f"Permission denied: {str(error)}")
ErrorData(code=-32000, message=f"Permission denied: {error!s}")
)
# asyncio.TimeoutError is a subclass of TimeoutError in Python 3.10, alias in 3.11+
elif error_type in (TimeoutError, asyncio.TimeoutError):
return McpError(
ErrorData(code=-32000, message=f"Request timeout: {str(error)}")
ErrorData(code=-32000, message=f"Request timeout: {error!s}")
)
else:
return McpError(
ErrorData(code=-32603, message=f"Internal error: {str(error)}")
ErrorData(code=-32603, message=f"Internal error: {error!s}")
)
async def on_message(self, context: MiddlewareContext, call_next: CallNext) -> Any:
@ -120,7 +120,7 @@ class ErrorHandlingMiddleware(Middleware):
# Transform and re-raise
transformed_error = self._transform_error(error)
raise transformed_error
raise transformed_error from error
def get_error_stats(self) -> dict[str, int]:
"""Get error statistics for monitoring."""
@ -200,7 +200,7 @@ class RetryMiddleware(Middleware):
delay = self._calculate_delay(attempt)
self.logger.warning(
f"Request {context.method} failed (attempt {attempt + 1}/{self.max_retries + 1}): "
f"{type(error).__name__}: {str(error)}. Retrying in {delay:.1f}s..."
f"{type(error).__name__}: {error!s}. Retrying in {delay:.1f}s..."
)
await anyio.sleep(delay)

View file

@ -27,9 +27,9 @@ if TYPE_CHECKING:
from fastmcp.server.context import Context
__all__ = [
"CallNext",
"Middleware",
"MiddlewareContext",
"CallNext",
]
logger = logging.getLogger(__name__)

View file

@ -513,11 +513,11 @@ class OpenAPITool(Tool):
if e.response.text:
error_message += f" - {e.response.text}"
raise ValueError(error_message)
raise ValueError(error_message) from e
except httpx.RequestError as e:
# Handle request errors (connection, timeout, etc.)
raise ValueError(f"Request error: {str(e)}")
raise ValueError(f"Request error: {e!s}") from e
class OpenAPIResource(Resource):
@ -531,9 +531,11 @@ class OpenAPIResource(Resource):
name: str,
description: str,
mime_type: str = "application/json",
tags: set[str] = set(),
tags: set[str] | None = None,
timeout: float | None = None,
):
if tags is None:
tags = set()
super().__init__(
uri=AnyUrl(uri), # Convert string to AnyUrl
name=name,
@ -632,11 +634,11 @@ class OpenAPIResource(Resource):
if e.response.text:
error_message += f" - {e.response.text}"
raise ValueError(error_message)
raise ValueError(error_message) from e
except httpx.RequestError as e:
# Handle request errors (connection, timeout, etc.)
raise ValueError(f"Request error: {str(e)}")
raise ValueError(f"Request error: {e!s}") from e
class OpenAPIResourceTemplate(ResourceTemplate):
@ -650,9 +652,11 @@ class OpenAPIResourceTemplate(ResourceTemplate):
name: str,
description: str,
parameters: dict[str, Any],
tags: set[str] = set(),
tags: set[str] | None = None,
timeout: float | None = None,
):
if tags is None:
tags = set()
super().__init__(
uri_template=uri_template,
name=name,

View file

@ -198,7 +198,9 @@ class ProxyResourceManager(ResourceManager, ProxyManagerMixin):
elif isinstance(result[0], BlobResourceContents):
return result[0].blob
else:
raise ResourceError(f"Unsupported content type: {type(result[0])}")
raise ResourceError(
f"Unsupported content type: {type(result[0])}"
) from None
class ProxyPromptManager(PromptManager, ProxyManagerMixin):
@ -558,7 +560,7 @@ class ProxyClient(Client[ClientTransportT]):
kwargs["log_handler"] = ProxyClient.default_log_handler
if "progress_handler" not in kwargs:
kwargs["progress_handler"] = ProxyClient.default_progress_handler
super().__init__(**kwargs | dict(transport=transport))
super().__init__(**kwargs | {"transport": transport})
@classmethod
async def default_sampling_handler(
@ -572,7 +574,7 @@ class ProxyClient(Client[ClientTransportT]):
"""
ctx = get_context()
content = await ctx.sample(
[msg for msg in messages],
list(messages),
system_prompt=params.systemPrompt,
temperature=params.temperature,
max_tokens=params.maxTokens,
@ -649,7 +651,6 @@ class StatefulProxyClient(ProxyClient[ClientTransportT]):
The stateful proxy client will be forced disconnected when the session is exited.
So we do nothing here.
"""
pass
async def clear(self):
"""

View file

@ -15,7 +15,11 @@ from collections.abc import (
Mapping,
Sequence,
)
from contextlib import AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager
from contextlib import (
AbstractAsyncContextManager,
AsyncExitStack,
asynccontextmanager,
)
from dataclasses import dataclass
from functools import partial
from pathlib import Path
@ -150,7 +154,7 @@ class FastMCP(Generic[LifespanResultT]):
version: str | None = None,
website_url: str | None = None,
icons: list[mcp.types.Icon] | None = None,
auth: AuthProvider | None | NotSetT = NotSet,
auth: AuthProvider | NotSetT | None = NotSet,
middleware: Sequence[Middleware] | None = None,
lifespan: LifespanCallable | None = None,
dependencies: list[str] | None = None,
@ -1062,10 +1066,10 @@ class FastMCP(Generic[LifespanResultT]):
try:
result = await self._call_tool_middleware(key, arguments)
return result.to_mcp_result()
except DisabledError:
raise NotFoundError(f"Unknown tool: {key}")
except NotFoundError:
raise NotFoundError(f"Unknown tool: {key}")
except DisabledError as e:
raise NotFoundError(f"Unknown tool: {key}") from e
except NotFoundError as e:
raise NotFoundError(f"Unknown tool: {key}") from e
async def _call_tool_middleware(
self,
@ -1142,12 +1146,12 @@ class FastMCP(Generic[LifespanResultT]):
return list[ReadResourceContents](
await self._read_resource_middleware(uri)
)
except DisabledError:
except DisabledError as e:
# convert to NotFoundError to avoid leaking resource presence
raise NotFoundError(f"Unknown resource: {str(uri)!r}")
except NotFoundError:
raise NotFoundError(f"Unknown resource: {str(uri)!r}") from e
except NotFoundError as e:
# standardize NotFound message
raise NotFoundError(f"Unknown resource: {str(uri)!r}")
raise NotFoundError(f"Unknown resource: {str(uri)!r}") from e
async def _read_resource_middleware(
self,
@ -1158,10 +1162,7 @@ class FastMCP(Generic[LifespanResultT]):
"""
# Convert string URI to AnyUrl if needed
if isinstance(uri, str):
uri_param = AnyUrl(uri)
else:
uri_param = uri
uri_param = AnyUrl(uri) if isinstance(uri, str) else uri
mw_context = MiddlewareContext(
message=mcp.types.ReadResourceRequestParams(uri=uri_param),
@ -1241,12 +1242,12 @@ class FastMCP(Generic[LifespanResultT]):
async with fastmcp.server.context.Context(fastmcp=self):
try:
return await self._get_prompt_middleware(name, arguments)
except DisabledError:
except DisabledError as e:
# convert to NotFoundError to avoid leaking prompt presence
raise NotFoundError(f"Unknown prompt: {name}")
except NotFoundError:
raise NotFoundError(f"Unknown prompt: {name}") from e
except NotFoundError as e:
# standardize NotFound message
raise NotFoundError(f"Unknown prompt: {name}")
raise NotFoundError(f"Unknown prompt: {name}") from e
async def _get_prompt_middleware(
self, name: str, arguments: dict[str, Any] | None = None
@ -1369,7 +1370,7 @@ class FastMCP(Generic[LifespanResultT]):
description: str | None = None,
icons: list[mcp.types.Icon] | None = None,
tags: set[str] | None = None,
output_schema: dict[str, Any] | None | NotSetT = NotSet,
output_schema: dict[str, Any] | NotSetT | None = NotSet,
annotations: ToolAnnotations | dict[str, Any] | None = None,
exclude_args: list[str] | None = None,
meta: dict[str, Any] | None = None,
@ -1386,7 +1387,7 @@ class FastMCP(Generic[LifespanResultT]):
description: str | None = None,
icons: list[mcp.types.Icon] | None = None,
tags: set[str] | None = None,
output_schema: dict[str, Any] | None | NotSetT = NotSet,
output_schema: dict[str, Any] | NotSetT | None = NotSet,
annotations: ToolAnnotations | dict[str, Any] | None = None,
exclude_args: list[str] | None = None,
meta: dict[str, Any] | None = None,
@ -1402,7 +1403,7 @@ class FastMCP(Generic[LifespanResultT]):
description: str | None = None,
icons: list[mcp.types.Icon] | None = None,
tags: set[str] | None = None,
output_schema: dict[str, Any] | None | NotSetT = NotSet,
output_schema: dict[str, Any] | NotSetT | None = NotSet,
annotations: ToolAnnotations | dict[str, Any] | None = None,
exclude_args: list[str] | None = None,
meta: dict[str, Any] | None = None,
@ -2029,14 +2030,14 @@ class FastMCP(Generic[LifespanResultT]):
port=port,
path=server_path,
)
_uvicorn_config_from_user = uvicorn_config or {}
uvicorn_config_from_user = uvicorn_config or {}
config_kwargs: dict[str, Any] = {
"timeout_graceful_shutdown": 0,
"lifespan": "on",
"ws": "websockets-sansio",
}
config_kwargs.update(_uvicorn_config_from_user)
config_kwargs.update(uvicorn_config_from_user)
if "log_config" not in config_kwargs and "log_level" not in config_kwargs:
config_kwargs["log_level"] = default_log_level_to_use
@ -2605,8 +2606,8 @@ class FastMCP(Generic[LifespanResultT]):
# - Connected clients: reuse existing session for all requests
# - Disconnected clients: create fresh sessions per request for isolation
if client.is_connected():
_proxy_logger = get_logger(__name__)
_proxy_logger.info(
proxy_logger = get_logger(__name__)
proxy_logger.info(
"Proxy detected connected client - reusing existing session for all requests. "
"This may cause context mixing in concurrent scenarios."
)
@ -2678,10 +2679,7 @@ class FastMCP(Generic[LifespanResultT]):
return False
if self.include_tags is not None:
if any(itag in component.tags for itag in self.include_tags):
return True
else:
return False
return bool(any(itag in component.tags for itag in self.include_tags))
return True

View file

@ -2,4 +2,4 @@ from .tool import Tool, FunctionTool
from .tool_manager import ToolManager
from .tool_transform import forward, forward_raw
__all__ = ["Tool", "ToolManager", "FunctionTool", "forward", "forward_raw"]
__all__ = ["FunctionTool", "Tool", "ToolManager", "forward", "forward_raw"]

View file

@ -173,7 +173,7 @@ class Tool(FastMCPComponent):
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,
output_schema: dict[str, Any] | Literal[False] | NotSetT | None = NotSet,
serializer: ToolResultSerializerType | None = None,
meta: dict[str, Any] | None = None,
enabled: bool | None = None,
@ -212,13 +212,13 @@ class Tool(FastMCPComponent):
tool: Tool,
*,
name: str | None = None,
title: str | None | NotSetT = NotSet,
description: str | None | NotSetT = NotSet,
title: str | NotSetT | None = NotSet,
description: str | NotSetT | None = NotSet,
tags: set[str] | None = None,
annotations: ToolAnnotations | None | NotSetT = NotSet,
output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet,
annotations: ToolAnnotations | NotSetT | None = NotSet,
output_schema: dict[str, Any] | Literal[False] | NotSetT | None = NotSet,
serializer: ToolResultSerializerType | None = None,
meta: dict[str, Any] | None | NotSetT = NotSet,
meta: dict[str, Any] | NotSetT | None = NotSet,
transform_args: dict[str, ArgTransform] | None = None,
enabled: bool | None = None,
transform_fn: Callable[..., Any] | None = None,
@ -255,7 +255,7 @@ class FunctionTool(Tool):
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,
output_schema: dict[str, Any] | Literal[False] | NotSetT | None = NotSet,
serializer: ToolResultSerializerType | None = None,
meta: dict[str, Any] | None = None,
enabled: bool | None = None,
@ -446,9 +446,8 @@ class ParsedFunction:
# we ensure that no output schema is automatically generated.
clean_output_type = replace_type(
output_type,
{
t: _UnserializableType
for t in (
dict.fromkeys( # type: ignore[arg-type]
(
Image,
Audio,
File,
@ -458,8 +457,9 @@ class ParsedFunction:
mcp.types.AudioContent,
mcp.types.ResourceLink,
mcp.types.EmbeddedResource,
)
},
),
_UnserializableType,
),
)
try:

View file

@ -365,15 +365,15 @@ class TransformedTool(Tool):
cls,
tool: Tool,
name: str | None = None,
title: str | None | NotSetT = NotSet,
description: str | None | NotSetT = NotSet,
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 | 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,
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.

View file

@ -240,12 +240,11 @@ def log_server_banner(
info_table.add_row("📦", "Transport:", display_transport)
# Show connection info based on transport
if transport in ("http", "streamable-http", "sse"):
if host and port:
server_url = f"http://{host}:{port}"
if path:
server_url += f"/{path.lstrip('/')}"
info_table.add_row("🔗", "Server URL:", server_url)
if transport in ("http", "streamable-http", "sse") and host and port:
server_url = f"http://{host}:{port}"
if path:
server_url += f"/{path.lstrip('/')}"
info_table.add_row("🔗", "Server URL:", server_url)
# Add documentation link
info_table.add_row("", "", "")

Some files were not shown because too many files have changed in this diff Show more