fastmcp 2.5.2__py3-none-any.whl → 2.6.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,325 @@
1
+ import secrets
2
+ import time
3
+
4
+ from mcp.server.auth.provider import (
5
+ AccessToken,
6
+ AuthorizationCode,
7
+ AuthorizationParams,
8
+ AuthorizeError,
9
+ RefreshToken,
10
+ TokenError,
11
+ construct_redirect_uri,
12
+ )
13
+ from mcp.shared.auth import (
14
+ OAuthClientInformationFull,
15
+ OAuthToken,
16
+ )
17
+ from pydantic import AnyHttpUrl
18
+
19
+ from fastmcp.server.auth.auth import (
20
+ ClientRegistrationOptions,
21
+ OAuthProvider,
22
+ RevocationOptions,
23
+ )
24
+
25
+ # Default expiration times (in seconds)
26
+ DEFAULT_AUTH_CODE_EXPIRY_SECONDS = 5 * 60 # 5 minutes
27
+ DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS = 60 * 60 # 1 hour
28
+ DEFAULT_REFRESH_TOKEN_EXPIRY_SECONDS = None # No expiry
29
+
30
+
31
+ class InMemoryOAuthProvider(OAuthProvider):
32
+ """
33
+ An in-memory OAuth provider for testing purposes.
34
+ It simulates the OAuth 2.1 flow locally without external calls.
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ issuer_url: AnyHttpUrl | str | None = None,
40
+ service_documentation_url: AnyHttpUrl | str | None = None,
41
+ client_registration_options: ClientRegistrationOptions | None = None,
42
+ revocation_options: RevocationOptions | None = None,
43
+ required_scopes: list[str] | None = None,
44
+ ):
45
+ super().__init__(
46
+ issuer_url=issuer_url or "http://fastmcp.example.com",
47
+ service_documentation_url=service_documentation_url,
48
+ client_registration_options=client_registration_options,
49
+ revocation_options=revocation_options,
50
+ required_scopes=required_scopes,
51
+ )
52
+ self.clients: dict[str, OAuthClientInformationFull] = {}
53
+ self.auth_codes: dict[str, AuthorizationCode] = {}
54
+ self.access_tokens: dict[str, AccessToken] = {}
55
+ self.refresh_tokens: dict[str, RefreshToken] = {}
56
+
57
+ # For revoking associated tokens
58
+ self._access_to_refresh_map: dict[
59
+ str, str
60
+ ] = {} # access_token_str -> refresh_token_str
61
+ self._refresh_to_access_map: dict[
62
+ str, str
63
+ ] = {} # refresh_token_str -> access_token_str
64
+
65
+ async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:
66
+ return self.clients.get(client_id)
67
+
68
+ async def register_client(self, client_info: OAuthClientInformationFull) -> None:
69
+ if client_info.client_id in self.clients:
70
+ # As per RFC 7591, if client_id is already known, it's an update.
71
+ # For this simple provider, we'll treat it as re-registration.
72
+ # A real provider might handle updates or raise errors for conflicts.
73
+ pass
74
+ self.clients[client_info.client_id] = client_info
75
+
76
+ async def authorize(
77
+ self, client: OAuthClientInformationFull, params: AuthorizationParams
78
+ ) -> str:
79
+ """
80
+ Simulates user authorization and generates an authorization code.
81
+ Returns a redirect URI with the code and state.
82
+ """
83
+ if client.client_id not in self.clients:
84
+ raise AuthorizeError(
85
+ error="unauthorized_client",
86
+ error_description=f"Client '{client.client_id}' not registered.",
87
+ )
88
+
89
+ # Validate redirect_uri (already validated by AuthorizationHandler, but good practice)
90
+ try:
91
+ # OAuthClientInformationFull should have a method like validate_redirect_uri
92
+ # For this test provider, we assume it's valid if it matches one in client_info
93
+ # The AuthorizationHandler already does robust validation using client.validate_redirect_uri
94
+ if params.redirect_uri not in client.redirect_uris:
95
+ # This check might be too simplistic if redirect_uris can be patterns
96
+ # or if params.redirect_uri is None and client has a default.
97
+ # However, the AuthorizationHandler handles the primary validation.
98
+ pass # Let's assume AuthorizationHandler did its job.
99
+ except Exception: # Replace with specific validation error if client.validate_redirect_uri existed
100
+ raise AuthorizeError(
101
+ error="invalid_request", error_description="Invalid redirect_uri."
102
+ )
103
+
104
+ auth_code_value = f"test_auth_code_{secrets.token_hex(16)}"
105
+ expires_at = time.time() + DEFAULT_AUTH_CODE_EXPIRY_SECONDS
106
+
107
+ # Ensure scopes are a list
108
+ scopes_list = params.scopes if params.scopes is not None else []
109
+ if client.scope: # Filter params.scopes against client's registered scopes
110
+ client_allowed_scopes = set(client.scope.split())
111
+ scopes_list = [s for s in scopes_list if s in client_allowed_scopes]
112
+
113
+ auth_code = AuthorizationCode(
114
+ code=auth_code_value,
115
+ client_id=client.client_id,
116
+ redirect_uri=params.redirect_uri,
117
+ redirect_uri_provided_explicitly=params.redirect_uri_provided_explicitly,
118
+ scopes=scopes_list,
119
+ expires_at=expires_at,
120
+ code_challenge=params.code_challenge,
121
+ # code_challenge_method is assumed S256 by the framework
122
+ )
123
+ self.auth_codes[auth_code_value] = auth_code
124
+
125
+ return construct_redirect_uri(
126
+ str(params.redirect_uri), code=auth_code_value, state=params.state
127
+ )
128
+
129
+ async def load_authorization_code(
130
+ self, client: OAuthClientInformationFull, authorization_code: str
131
+ ) -> AuthorizationCode | None:
132
+ auth_code_obj = self.auth_codes.get(authorization_code)
133
+ if auth_code_obj:
134
+ if auth_code_obj.client_id != client.client_id:
135
+ return None # Belongs to a different client
136
+ if auth_code_obj.expires_at < time.time():
137
+ del self.auth_codes[authorization_code] # Expired
138
+ return None
139
+ return auth_code_obj
140
+ return None
141
+
142
+ async def exchange_authorization_code(
143
+ self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode
144
+ ) -> OAuthToken:
145
+ # Authorization code should have been validated (existence, expiry, client_id match)
146
+ # by the TokenHandler calling load_authorization_code before this.
147
+ # We might want to re-verify or simply trust it's valid.
148
+
149
+ if authorization_code.code not in self.auth_codes:
150
+ raise TokenError(
151
+ "invalid_grant", "Authorization code not found or already used."
152
+ )
153
+
154
+ # Consume the auth code
155
+ del self.auth_codes[authorization_code.code]
156
+
157
+ access_token_value = f"test_access_token_{secrets.token_hex(32)}"
158
+ refresh_token_value = f"test_refresh_token_{secrets.token_hex(32)}"
159
+
160
+ access_token_expires_at = int(time.time() + DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS)
161
+
162
+ # Refresh token expiry
163
+ refresh_token_expires_at = None
164
+ if DEFAULT_REFRESH_TOKEN_EXPIRY_SECONDS is not None:
165
+ refresh_token_expires_at = int(
166
+ time.time() + DEFAULT_REFRESH_TOKEN_EXPIRY_SECONDS
167
+ )
168
+
169
+ self.access_tokens[access_token_value] = AccessToken(
170
+ token=access_token_value,
171
+ client_id=client.client_id,
172
+ scopes=authorization_code.scopes,
173
+ expires_at=access_token_expires_at,
174
+ )
175
+ self.refresh_tokens[refresh_token_value] = RefreshToken(
176
+ token=refresh_token_value,
177
+ client_id=client.client_id,
178
+ scopes=authorization_code.scopes, # Refresh token inherits scopes
179
+ expires_at=refresh_token_expires_at,
180
+ )
181
+
182
+ self._access_to_refresh_map[access_token_value] = refresh_token_value
183
+ self._refresh_to_access_map[refresh_token_value] = access_token_value
184
+
185
+ return OAuthToken(
186
+ access_token=access_token_value,
187
+ token_type="bearer",
188
+ expires_in=DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS,
189
+ refresh_token=refresh_token_value,
190
+ scope=" ".join(authorization_code.scopes),
191
+ )
192
+
193
+ async def load_refresh_token(
194
+ self, client: OAuthClientInformationFull, refresh_token: str
195
+ ) -> RefreshToken | None:
196
+ token_obj = self.refresh_tokens.get(refresh_token)
197
+ if token_obj:
198
+ if token_obj.client_id != client.client_id:
199
+ return None # Belongs to different client
200
+ if token_obj.expires_at is not None and token_obj.expires_at < time.time():
201
+ self._revoke_internal(
202
+ refresh_token_str=token_obj.token
203
+ ) # Clean up expired
204
+ return None
205
+ return token_obj
206
+ return None
207
+
208
+ async def exchange_refresh_token(
209
+ self,
210
+ client: OAuthClientInformationFull,
211
+ refresh_token: RefreshToken, # This is the RefreshToken object, already loaded
212
+ scopes: list[str], # Requested scopes for the new access token
213
+ ) -> OAuthToken:
214
+ # Validate scopes: requested scopes must be a subset of original scopes
215
+ original_scopes = set(refresh_token.scopes)
216
+ requested_scopes = set(scopes)
217
+ if not requested_scopes.issubset(original_scopes):
218
+ raise TokenError(
219
+ "invalid_scope",
220
+ "Requested scopes exceed those authorized by the refresh token.",
221
+ )
222
+
223
+ # Invalidate old refresh token and its associated access token (rotation)
224
+ self._revoke_internal(refresh_token_str=refresh_token.token)
225
+
226
+ # Issue new tokens
227
+ new_access_token_value = f"test_access_token_{secrets.token_hex(32)}"
228
+ new_refresh_token_value = f"test_refresh_token_{secrets.token_hex(32)}"
229
+
230
+ access_token_expires_at = int(time.time() + DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS)
231
+
232
+ # Refresh token expiry
233
+ refresh_token_expires_at = None
234
+ if DEFAULT_REFRESH_TOKEN_EXPIRY_SECONDS is not None:
235
+ refresh_token_expires_at = int(
236
+ time.time() + DEFAULT_REFRESH_TOKEN_EXPIRY_SECONDS
237
+ )
238
+
239
+ self.access_tokens[new_access_token_value] = AccessToken(
240
+ token=new_access_token_value,
241
+ client_id=client.client_id,
242
+ scopes=scopes, # Use newly requested (and validated) scopes
243
+ expires_at=access_token_expires_at,
244
+ )
245
+ self.refresh_tokens[new_refresh_token_value] = RefreshToken(
246
+ token=new_refresh_token_value,
247
+ client_id=client.client_id,
248
+ scopes=scopes, # New refresh token also gets these scopes
249
+ expires_at=refresh_token_expires_at,
250
+ )
251
+
252
+ self._access_to_refresh_map[new_access_token_value] = new_refresh_token_value
253
+ self._refresh_to_access_map[new_refresh_token_value] = new_access_token_value
254
+
255
+ return OAuthToken(
256
+ access_token=new_access_token_value,
257
+ token_type="bearer",
258
+ expires_in=DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS,
259
+ refresh_token=new_refresh_token_value,
260
+ scope=" ".join(scopes),
261
+ )
262
+
263
+ async def load_access_token(self, token: str) -> AccessToken | None:
264
+ token_obj = self.access_tokens.get(token)
265
+ if token_obj:
266
+ if token_obj.expires_at is not None and token_obj.expires_at < time.time():
267
+ self._revoke_internal(
268
+ access_token_str=token_obj.token
269
+ ) # Clean up expired
270
+ return None
271
+ return token_obj
272
+ return None
273
+
274
+ def _revoke_internal(
275
+ self, access_token_str: str | None = None, refresh_token_str: str | None = None
276
+ ):
277
+ """Internal helper to remove tokens and their associations."""
278
+ removed_access_token = None
279
+ removed_refresh_token = None
280
+
281
+ if access_token_str:
282
+ if access_token_str in self.access_tokens:
283
+ del self.access_tokens[access_token_str]
284
+ removed_access_token = access_token_str
285
+
286
+ # Get associated refresh token
287
+ associated_refresh = self._access_to_refresh_map.pop(access_token_str, None)
288
+ if associated_refresh:
289
+ if associated_refresh in self.refresh_tokens:
290
+ del self.refresh_tokens[associated_refresh]
291
+ removed_refresh_token = associated_refresh
292
+ self._refresh_to_access_map.pop(associated_refresh, None)
293
+
294
+ if refresh_token_str:
295
+ if refresh_token_str in self.refresh_tokens:
296
+ del self.refresh_tokens[refresh_token_str]
297
+ removed_refresh_token = refresh_token_str
298
+
299
+ # Get associated access token
300
+ associated_access = self._refresh_to_access_map.pop(refresh_token_str, None)
301
+ if associated_access:
302
+ if associated_access in self.access_tokens:
303
+ del self.access_tokens[associated_access]
304
+ removed_access_token = associated_access
305
+ self._access_to_refresh_map.pop(associated_access, None)
306
+
307
+ # Clean up any dangling references if one part of the pair was already gone
308
+ if removed_access_token and removed_access_token in self._access_to_refresh_map:
309
+ del self._access_to_refresh_map[removed_access_token]
310
+ if (
311
+ removed_refresh_token
312
+ and removed_refresh_token in self._refresh_to_access_map
313
+ ):
314
+ del self._refresh_to_access_map[removed_refresh_token]
315
+
316
+ async def revoke_token(
317
+ self,
318
+ token: AccessToken | RefreshToken,
319
+ ) -> None:
320
+ """Revokes an access or refresh token and its counterpart."""
321
+ if isinstance(token, AccessToken):
322
+ self._revoke_internal(access_token_str=token.token)
323
+ elif isinstance(token, RefreshToken):
324
+ self._revoke_internal(refresh_token_str=token.token)
325
+ # If token is not found or already revoked, _revoke_internal does nothing, which is correct.
@@ -2,6 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, ParamSpec, TypeVar
4
4
 
5
+ from mcp.server.auth.middleware.auth_context import get_access_token
6
+ from mcp.server.auth.provider import AccessToken
5
7
  from starlette.requests import Request
6
8
 
7
9
  if TYPE_CHECKING:
@@ -10,6 +12,14 @@ if TYPE_CHECKING:
10
12
  P = ParamSpec("P")
11
13
  R = TypeVar("R")
12
14
 
15
+ __all__ = [
16
+ "get_context",
17
+ "get_http_request",
18
+ "get_http_headers",
19
+ "get_access_token",
20
+ "AccessToken",
21
+ ]
22
+
13
23
 
14
24
  # --- Context ---
15
25
 
fastmcp/server/http.py CHANGED
@@ -10,14 +10,7 @@ from mcp.server.auth.middleware.bearer_auth import (
10
10
  BearerAuthBackend,
11
11
  RequireAuthMiddleware,
12
12
  )
13
- from mcp.server.auth.provider import (
14
- AccessTokenT,
15
- AuthorizationCodeT,
16
- OAuthAuthorizationServerProvider,
17
- RefreshTokenT,
18
- )
19
13
  from mcp.server.auth.routes import create_auth_routes
20
- from mcp.server.auth.settings import AuthSettings
21
14
  from mcp.server.lowlevel.server import LifespanResultT
22
15
  from mcp.server.sse import SseServerTransport
23
16
  from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
@@ -29,6 +22,7 @@ from starlette.responses import Response
29
22
  from starlette.routing import BaseRoute, Mount, Route
30
23
  from starlette.types import Lifespan, Receive, Scope, Send
31
24
 
25
+ from fastmcp.server.auth.auth import OAuthProvider
32
26
  from fastmcp.utilities.logging import get_logger
33
27
 
34
28
  if TYPE_CHECKING:
@@ -75,17 +69,12 @@ class RequestContextMiddleware:
75
69
 
76
70
 
77
71
  def setup_auth_middleware_and_routes(
78
- auth_server_provider: OAuthAuthorizationServerProvider[
79
- AuthorizationCodeT, RefreshTokenT, AccessTokenT
80
- ]
81
- | None,
82
- auth_settings: AuthSettings | None,
72
+ auth: OAuthProvider,
83
73
  ) -> tuple[list[Middleware], list[BaseRoute], list[str]]:
84
74
  """Set up authentication middleware and routes if auth is enabled.
85
75
 
86
76
  Args:
87
- auth_server_provider: The OAuth authorization server provider
88
- auth_settings: The auth settings
77
+ auth: The OAuthProvider authorization server provider
89
78
 
90
79
  Returns:
91
80
  Tuple of (middleware, auth_routes, required_scopes)
@@ -94,31 +83,25 @@ def setup_auth_middleware_and_routes(
94
83
  auth_routes: list[BaseRoute] = []
95
84
  required_scopes: list[str] = []
96
85
 
97
- if auth_server_provider:
98
- if not auth_settings:
99
- raise ValueError(
100
- "auth_settings must be provided when auth_server_provider is specified"
101
- )
86
+ middleware = [
87
+ Middleware(
88
+ AuthenticationMiddleware,
89
+ backend=BearerAuthBackend(provider=auth),
90
+ ),
91
+ Middleware(AuthContextMiddleware),
92
+ ]
102
93
 
103
- middleware = [
104
- Middleware(
105
- AuthenticationMiddleware,
106
- backend=BearerAuthBackend(provider=auth_server_provider),
107
- ),
108
- Middleware(AuthContextMiddleware),
109
- ]
110
-
111
- required_scopes = auth_settings.required_scopes or []
112
-
113
- auth_routes.extend(
114
- create_auth_routes(
115
- provider=auth_server_provider,
116
- issuer_url=auth_settings.issuer_url,
117
- service_documentation_url=auth_settings.service_documentation_url,
118
- client_registration_options=auth_settings.client_registration_options,
119
- revocation_options=auth_settings.revocation_options,
120
- )
94
+ required_scopes = auth.required_scopes or []
95
+
96
+ auth_routes.extend(
97
+ create_auth_routes(
98
+ provider=auth,
99
+ issuer_url=auth.issuer_url,
100
+ service_documentation_url=auth.service_documentation_url,
101
+ client_registration_options=auth.client_registration_options,
102
+ revocation_options=auth.revocation_options,
121
103
  )
104
+ )
122
105
 
123
106
  return middleware, auth_routes, required_scopes
124
107
 
@@ -155,11 +138,7 @@ def create_sse_app(
155
138
  server: FastMCP[LifespanResultT],
156
139
  message_path: str,
157
140
  sse_path: str,
158
- auth_server_provider: OAuthAuthorizationServerProvider[
159
- AuthorizationCodeT, RefreshTokenT, AccessTokenT
160
- ]
161
- | None = None,
162
- auth_settings: AuthSettings | None = None,
141
+ auth: OAuthProvider | None = None,
163
142
  debug: bool = False,
164
143
  routes: list[BaseRoute] | None = None,
165
144
  middleware: list[Middleware] | None = None,
@@ -170,8 +149,7 @@ def create_sse_app(
170
149
  server: The FastMCP server instance
171
150
  message_path: Path for SSE messages
172
151
  sse_path: Path for SSE connections
173
- auth_server_provider: Optional auth provider
174
- auth_settings: Optional auth settings
152
+ auth: Optional auth provider
175
153
  debug: Whether to enable debug mode
176
154
  routes: Optional list of custom routes
177
155
  middleware: Optional list of middleware
@@ -196,15 +174,15 @@ def create_sse_app(
196
174
  return Response()
197
175
 
198
176
  # Get auth middleware and routes
199
- auth_middleware, auth_routes, required_scopes = setup_auth_middleware_and_routes(
200
- auth_server_provider, auth_settings
201
- )
202
-
203
- server_routes.extend(auth_routes)
204
- server_middleware.extend(auth_middleware)
205
177
 
206
178
  # Add SSE routes with or without auth
207
- if auth_server_provider:
179
+ if auth:
180
+ auth_middleware, auth_routes, required_scopes = (
181
+ setup_auth_middleware_and_routes(auth)
182
+ )
183
+
184
+ server_routes.extend(auth_routes)
185
+ server_middleware.extend(auth_middleware)
208
186
  # Auth is enabled, wrap endpoints with RequireAuthMiddleware
209
187
  server_routes.append(
210
188
  Route(
@@ -264,11 +242,7 @@ def create_streamable_http_app(
264
242
  server: FastMCP[LifespanResultT],
265
243
  streamable_http_path: str,
266
244
  event_store: None = None,
267
- auth_server_provider: OAuthAuthorizationServerProvider[
268
- AuthorizationCodeT, RefreshTokenT, AccessTokenT
269
- ]
270
- | None = None,
271
- auth_settings: AuthSettings | None = None,
245
+ auth: OAuthProvider | None = None,
272
246
  json_response: bool = False,
273
247
  stateless_http: bool = False,
274
248
  debug: bool = False,
@@ -281,8 +255,7 @@ def create_streamable_http_app(
281
255
  server: The FastMCP server instance
282
256
  streamable_http_path: Path for StreamableHTTP connections
283
257
  event_store: Optional event store for session management
284
- auth_server_provider: Optional auth provider
285
- auth_settings: Optional auth settings
258
+ auth: Optional auth provider
286
259
  json_response: Whether to use JSON response format
287
260
  stateless_http: Whether to use stateless mode (new transport per request)
288
261
  debug: Whether to enable debug mode
@@ -331,16 +304,15 @@ def create_streamable_http_app(
331
304
  # Re-raise other RuntimeErrors if they don't match the specific message
332
305
  raise
333
306
 
334
- # Get auth middleware and routes
335
- auth_middleware, auth_routes, required_scopes = setup_auth_middleware_and_routes(
336
- auth_server_provider, auth_settings
337
- )
307
+ # Add StreamableHTTP routes with or without auth
308
+ if auth:
309
+ auth_middleware, auth_routes, required_scopes = (
310
+ setup_auth_middleware_and_routes(auth)
311
+ )
338
312
 
339
- server_routes.extend(auth_routes)
340
- server_middleware.extend(auth_middleware)
313
+ server_routes.extend(auth_routes)
314
+ server_middleware.extend(auth_middleware)
341
315
 
342
- # Add StreamableHTTP routes with or without auth
343
- if auth_server_provider:
344
316
  # Auth is enabled, wrap endpoint with RequireAuthMiddleware
345
317
  server_routes.append(
346
318
  Mount(
fastmcp/server/openapi.py CHANGED
@@ -226,6 +226,7 @@ class OpenAPITool(Tool):
226
226
  tags: set[str] = set(),
227
227
  timeout: float | None = None,
228
228
  annotations: ToolAnnotations | None = None,
229
+ exclude_args: list[str] | None = None,
229
230
  serializer: Callable[[Any], str] | None = None,
230
231
  ):
231
232
  super().__init__(
@@ -235,6 +236,7 @@ class OpenAPITool(Tool):
235
236
  fn=self._execute_request, # We'll use an instance method instead of a global function
236
237
  tags=tags,
237
238
  annotations=annotations,
239
+ exclude_args=exclude_args,
238
240
  serializer=serializer,
239
241
  )
240
242
  self._client = client
fastmcp/server/server.py CHANGED
@@ -18,7 +18,6 @@ from typing import TYPE_CHECKING, Any, Generic, Literal
18
18
  import anyio
19
19
  import httpx
20
20
  import uvicorn
21
- from mcp.server.auth.provider import OAuthAuthorizationServerProvider
22
21
  from mcp.server.lowlevel.helper_types import ReadResourceContents
23
22
  from mcp.server.lowlevel.server import LifespanResultT, NotificationOptions
24
23
  from mcp.server.lowlevel.server import Server as MCPServer
@@ -48,6 +47,8 @@ from fastmcp.prompts import Prompt, PromptManager
48
47
  from fastmcp.prompts.prompt import PromptResult
49
48
  from fastmcp.resources import Resource, ResourceManager
50
49
  from fastmcp.resources.template import ResourceTemplate
50
+ from fastmcp.server.auth.auth import OAuthProvider
51
+ from fastmcp.server.auth.providers.bearer_env import EnvBearerAuthProvider
51
52
  from fastmcp.server.http import (
52
53
  StarletteWithLifespan,
53
54
  create_sse_app,
@@ -110,8 +111,7 @@ class FastMCP(Generic[LifespanResultT]):
110
111
  self,
111
112
  name: str | None = None,
112
113
  instructions: str | None = None,
113
- auth_server_provider: OAuthAuthorizationServerProvider[Any, Any, Any]
114
- | None = None,
114
+ auth: OAuthProvider | None = None,
115
115
  lifespan: (
116
116
  Callable[
117
117
  [FastMCP[LifespanResultT]],
@@ -128,18 +128,9 @@ class FastMCP(Generic[LifespanResultT]):
128
128
  on_duplicate_prompts: DuplicateBehavior | None = None,
129
129
  resource_prefix_format: Literal["protocol", "path"] | None = None,
130
130
  mask_error_details: bool | None = None,
131
+ tools: list[Tool | Callable[..., Any]] | None = None,
131
132
  **settings: Any,
132
133
  ):
133
- if settings:
134
- # TODO: remove settings. Deprecated since 2.3.4
135
- warnings.warn(
136
- "Passing runtime and transport-specific settings as kwargs "
137
- "to the FastMCP constructor is deprecated (as of 2.3.4), "
138
- "including most transport settings. If possible, provide settings when calling "
139
- "run() instead.",
140
- DeprecationWarning,
141
- stacklevel=2,
142
- )
143
134
  self.settings = fastmcp.settings.ServerSettings(**settings)
144
135
 
145
136
  # If mask_error_details is provided, override the settings value
@@ -186,13 +177,16 @@ class FastMCP(Generic[LifespanResultT]):
186
177
  lifespan=_lifespan_wrapper(self, lifespan),
187
178
  )
188
179
 
189
- if (self.settings.auth is not None) != (auth_server_provider is not None):
190
- # TODO: after we support separate authorization servers (see
191
- raise ValueError(
192
- "settings.auth must be specified if and only if auth_server_provider "
193
- "is specified"
194
- )
195
- self._auth_server_provider = auth_server_provider
180
+ if auth is None and self.settings.default_auth_provider == "bearer_env":
181
+ auth = EnvBearerAuthProvider()
182
+ self.auth = auth
183
+
184
+ if tools:
185
+ for tool in tools:
186
+ if isinstance(tool, Tool):
187
+ self._tool_manager.add_tool(tool)
188
+ else:
189
+ self.add_tool(tool)
196
190
 
197
191
  # Set up MCP protocol handlers
198
192
  self._setup_handlers()
@@ -497,6 +491,7 @@ class FastMCP(Generic[LifespanResultT]):
497
491
  description: str | None = None,
498
492
  tags: set[str] | None = None,
499
493
  annotations: ToolAnnotations | dict[str, Any] | None = None,
494
+ exclude_args: list[str] | None = None,
500
495
  ) -> None:
501
496
  """Add a tool to the server.
502
497
 
@@ -519,6 +514,7 @@ class FastMCP(Generic[LifespanResultT]):
519
514
  description=description,
520
515
  tags=tags,
521
516
  annotations=annotations,
517
+ exclude_args=exclude_args,
522
518
  )
523
519
  self._cache.clear()
524
520
 
@@ -540,6 +536,7 @@ class FastMCP(Generic[LifespanResultT]):
540
536
  description: str | None = None,
541
537
  tags: set[str] | None = None,
542
538
  annotations: ToolAnnotations | dict[str, Any] | None = None,
539
+ exclude_args: list[str] | None = None,
543
540
  ) -> Callable[[AnyFunction], AnyFunction]:
544
541
  """Decorator to register a tool.
545
542
 
@@ -583,6 +580,7 @@ class FastMCP(Generic[LifespanResultT]):
583
580
  description=description,
584
581
  tags=tags,
585
582
  annotations=annotations,
583
+ exclude_args=exclude_args,
586
584
  )
587
585
  return fn
588
586
 
@@ -903,8 +901,7 @@ class FastMCP(Generic[LifespanResultT]):
903
901
  server=self,
904
902
  message_path=message_path or self.settings.message_path,
905
903
  sse_path=path or self.settings.sse_path,
906
- auth_server_provider=self._auth_server_provider,
907
- auth_settings=self.settings.auth,
904
+ auth=self.auth,
908
905
  debug=self.settings.debug,
909
906
  middleware=middleware,
910
907
  )
@@ -951,8 +948,7 @@ class FastMCP(Generic[LifespanResultT]):
951
948
  server=self,
952
949
  streamable_http_path=path or self.settings.streamable_http_path,
953
950
  event_store=None,
954
- auth_server_provider=self._auth_server_provider,
955
- auth_settings=self.settings.auth,
951
+ auth=self.auth,
956
952
  json_response=self.settings.json_response,
957
953
  stateless_http=self.settings.stateless_http,
958
954
  debug=self.settings.debug,
@@ -963,8 +959,7 @@ class FastMCP(Generic[LifespanResultT]):
963
959
  server=self,
964
960
  message_path=self.settings.message_path,
965
961
  sse_path=path or self.settings.sse_path,
966
- auth_server_provider=self._auth_server_provider,
967
- auth_settings=self.settings.auth,
962
+ auth=self.auth,
968
963
  debug=self.settings.debug,
969
964
  middleware=middleware,
970
965
  )