mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-23 22:14:18 +02:00
Fix merge conflicts in context.py
This commit is contained in:
commit
38af6228f4
120 changed files with 2280 additions and 1087 deletions
3
.github/workflows/marvin.yml
vendored
3
.github/workflows/marvin.yml
vendored
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
25
.github/workflows/run-tests.yml
vendored
25
.github/workflows/run-tests.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)**
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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!")
|
||||
|
||||
|
|
|
|||
|
|
@ -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__":
|
||||
|
|
|
|||
|
|
@ -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!")
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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__":
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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!")
|
||||
|
||||
|
|
|
|||
|
|
@ -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!")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
26
docs/python-sdk/fastmcp-server-auth-middleware.mdx
Normal file
26
docs/python-sdk/fastmcp-server-auth-middleware.mdx
Normal 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.
|
||||
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
91
docs/python-sdk/fastmcp-server-middleware-tool_injection.mdx
Normal file
91
docs/python-sdk/fastmcp-server-middleware-tool_injection.mdx
Normal 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.
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
# /// script
|
||||
# dependencies = ["aiohttp", "fastmcp"]
|
||||
# ///
|
||||
|
||||
# uv pip install aiohttp fastmcp
|
||||
|
||||
import aiohttp
|
||||
|
||||
from fastmcp.server import FastMCP
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -48,9 +48,9 @@ def __getattr__(name: str):
|
|||
|
||||
|
||||
__all__ = [
|
||||
"FastMCP",
|
||||
"Context",
|
||||
"client",
|
||||
"Client",
|
||||
"Context",
|
||||
"FastMCP",
|
||||
"client",
|
||||
"settings",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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[
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]):
|
||||
|
|
|
|||
|
|
@ -79,10 +79,10 @@ class HTTPRoute(FastMCPBaseModel):
|
|||
# Export public symbols
|
||||
__all__ = [
|
||||
"HTTPRoute",
|
||||
"HttpMethod",
|
||||
"JsonSchema",
|
||||
"ParameterInfo",
|
||||
"ParameterLocation",
|
||||
"RequestBodyInfo",
|
||||
"ResponseInfo",
|
||||
"HttpMethod",
|
||||
"ParameterLocation",
|
||||
"JsonSchema",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ from .prompt import Prompt, PromptMessage, Message
|
|||
from .prompt_manager import PromptManager
|
||||
|
||||
__all__ = [
|
||||
"Message",
|
||||
"Prompt",
|
||||
"PromptManager",
|
||||
"PromptMessage",
|
||||
"Message",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -3,4 +3,4 @@ from .context import Context
|
|||
from . import dependencies
|
||||
|
||||
|
||||
__all__ = ["FastMCP", "Context"]
|
||||
__all__ = ["Context", "FastMCP"]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
114
src/fastmcp/server/auth/providers/debug.py
Normal file
114
src/fastmcp/server/auth/providers/debug.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ __all__ = [
|
|||
"AcceptedElicitation",
|
||||
"CancelledElicitation",
|
||||
"DeclinedElicitation",
|
||||
"get_elicitation_schema",
|
||||
"ScalarElicitationType",
|
||||
"get_elicitation_schema",
|
||||
]
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from .middleware import (
|
|||
)
|
||||
|
||||
__all__ = [
|
||||
"CallNext",
|
||||
"Middleware",
|
||||
"MiddlewareContext",
|
||||
"CallNext",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@ if TYPE_CHECKING:
|
|||
from fastmcp.server.context import Context
|
||||
|
||||
__all__ = [
|
||||
"CallNext",
|
||||
"Middleware",
|
||||
"MiddlewareContext",
|
||||
"CallNext",
|
||||
]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue