fastmcp 2.5.2__py3-none-any.whl → 2.6.0__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,330 @@
1
+ """
2
+ This is a simple in-memory OAuth provider for testing purposes.
3
+ It simulates the OAuth 2.0 flow locally without external calls.
4
+ """
5
+
6
+ import secrets
7
+ import time
8
+
9
+ from mcp.server.auth.provider import (
10
+ AccessToken,
11
+ AuthorizationCode,
12
+ AuthorizationParams,
13
+ AuthorizeError,
14
+ RefreshToken,
15
+ TokenError,
16
+ construct_redirect_uri,
17
+ )
18
+ from mcp.shared.auth import (
19
+ OAuthClientInformationFull,
20
+ OAuthToken,
21
+ )
22
+ from pydantic import AnyHttpUrl
23
+
24
+ from fastmcp.server.auth.auth import (
25
+ ClientRegistrationOptions,
26
+ OAuthProvider,
27
+ RevocationOptions,
28
+ )
29
+
30
+ # Default expiration times (in seconds)
31
+ DEFAULT_AUTH_CODE_EXPIRY_SECONDS = 5 * 60 # 5 minutes
32
+ DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS = 60 * 60 # 1 hour
33
+ DEFAULT_REFRESH_TOKEN_EXPIRY_SECONDS = None # No expiry
34
+
35
+
36
+ class InMemoryOAuthProvider(OAuthProvider):
37
+ """
38
+ An in-memory OAuth provider for testing purposes.
39
+ It simulates the OAuth 2.0 flow locally without external calls.
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ issuer_url: AnyHttpUrl | str | None = None,
45
+ service_documentation_url: AnyHttpUrl | str | None = None,
46
+ client_registration_options: ClientRegistrationOptions | None = None,
47
+ revocation_options: RevocationOptions | None = None,
48
+ required_scopes: list[str] | None = None,
49
+ ):
50
+ super().__init__(
51
+ issuer_url=issuer_url or "http://fastmcp.example.com",
52
+ service_documentation_url=service_documentation_url,
53
+ client_registration_options=client_registration_options,
54
+ revocation_options=revocation_options,
55
+ required_scopes=required_scopes,
56
+ )
57
+ self.clients: dict[str, OAuthClientInformationFull] = {}
58
+ self.auth_codes: dict[str, AuthorizationCode] = {}
59
+ self.access_tokens: dict[str, AccessToken] = {}
60
+ self.refresh_tokens: dict[str, RefreshToken] = {}
61
+
62
+ # For revoking associated tokens
63
+ self._access_to_refresh_map: dict[
64
+ str, str
65
+ ] = {} # access_token_str -> refresh_token_str
66
+ self._refresh_to_access_map: dict[
67
+ str, str
68
+ ] = {} # refresh_token_str -> access_token_str
69
+
70
+ async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:
71
+ return self.clients.get(client_id)
72
+
73
+ async def register_client(self, client_info: OAuthClientInformationFull) -> None:
74
+ if client_info.client_id in self.clients:
75
+ # As per RFC 7591, if client_id is already known, it's an update.
76
+ # For this simple provider, we'll treat it as re-registration.
77
+ # A real provider might handle updates or raise errors for conflicts.
78
+ pass
79
+ self.clients[client_info.client_id] = client_info
80
+
81
+ async def authorize(
82
+ self, client: OAuthClientInformationFull, params: AuthorizationParams
83
+ ) -> str:
84
+ """
85
+ Simulates user authorization and generates an authorization code.
86
+ Returns a redirect URI with the code and state.
87
+ """
88
+ if client.client_id not in self.clients:
89
+ raise AuthorizeError(
90
+ error="unauthorized_client",
91
+ error_description=f"Client '{client.client_id}' not registered.",
92
+ )
93
+
94
+ # Validate redirect_uri (already validated by AuthorizationHandler, but good practice)
95
+ try:
96
+ # OAuthClientInformationFull should have a method like validate_redirect_uri
97
+ # For this test provider, we assume it's valid if it matches one in client_info
98
+ # The AuthorizationHandler already does robust validation using client.validate_redirect_uri
99
+ if params.redirect_uri not in client.redirect_uris:
100
+ # This check might be too simplistic if redirect_uris can be patterns
101
+ # or if params.redirect_uri is None and client has a default.
102
+ # However, the AuthorizationHandler handles the primary validation.
103
+ pass # Let's assume AuthorizationHandler did its job.
104
+ except Exception: # Replace with specific validation error if client.validate_redirect_uri existed
105
+ raise AuthorizeError(
106
+ error="invalid_request", error_description="Invalid redirect_uri."
107
+ )
108
+
109
+ auth_code_value = f"test_auth_code_{secrets.token_hex(16)}"
110
+ expires_at = time.time() + DEFAULT_AUTH_CODE_EXPIRY_SECONDS
111
+
112
+ # Ensure scopes are a list
113
+ scopes_list = params.scopes if params.scopes is not None else []
114
+ if client.scope: # Filter params.scopes against client's registered scopes
115
+ client_allowed_scopes = set(client.scope.split())
116
+ scopes_list = [s for s in scopes_list if s in client_allowed_scopes]
117
+
118
+ auth_code = AuthorizationCode(
119
+ code=auth_code_value,
120
+ client_id=client.client_id,
121
+ redirect_uri=params.redirect_uri,
122
+ redirect_uri_provided_explicitly=params.redirect_uri_provided_explicitly,
123
+ scopes=scopes_list,
124
+ expires_at=expires_at,
125
+ code_challenge=params.code_challenge,
126
+ # code_challenge_method is assumed S256 by the framework
127
+ )
128
+ self.auth_codes[auth_code_value] = auth_code
129
+
130
+ return construct_redirect_uri(
131
+ str(params.redirect_uri), code=auth_code_value, state=params.state
132
+ )
133
+
134
+ async def load_authorization_code(
135
+ self, client: OAuthClientInformationFull, authorization_code: str
136
+ ) -> AuthorizationCode | None:
137
+ auth_code_obj = self.auth_codes.get(authorization_code)
138
+ if auth_code_obj:
139
+ if auth_code_obj.client_id != client.client_id:
140
+ return None # Belongs to a different client
141
+ if auth_code_obj.expires_at < time.time():
142
+ del self.auth_codes[authorization_code] # Expired
143
+ return None
144
+ return auth_code_obj
145
+ return None
146
+
147
+ async def exchange_authorization_code(
148
+ self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode
149
+ ) -> OAuthToken:
150
+ # Authorization code should have been validated (existence, expiry, client_id match)
151
+ # by the TokenHandler calling load_authorization_code before this.
152
+ # We might want to re-verify or simply trust it's valid.
153
+
154
+ if authorization_code.code not in self.auth_codes:
155
+ raise TokenError(
156
+ "invalid_grant", "Authorization code not found or already used."
157
+ )
158
+
159
+ # Consume the auth code
160
+ del self.auth_codes[authorization_code.code]
161
+
162
+ access_token_value = f"test_access_token_{secrets.token_hex(32)}"
163
+ refresh_token_value = f"test_refresh_token_{secrets.token_hex(32)}"
164
+
165
+ access_token_expires_at = int(time.time() + DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS)
166
+
167
+ # Refresh token expiry
168
+ refresh_token_expires_at = None
169
+ if DEFAULT_REFRESH_TOKEN_EXPIRY_SECONDS is not None:
170
+ refresh_token_expires_at = int(
171
+ time.time() + DEFAULT_REFRESH_TOKEN_EXPIRY_SECONDS
172
+ )
173
+
174
+ self.access_tokens[access_token_value] = AccessToken(
175
+ token=access_token_value,
176
+ client_id=client.client_id,
177
+ scopes=authorization_code.scopes,
178
+ expires_at=access_token_expires_at,
179
+ )
180
+ self.refresh_tokens[refresh_token_value] = RefreshToken(
181
+ token=refresh_token_value,
182
+ client_id=client.client_id,
183
+ scopes=authorization_code.scopes, # Refresh token inherits scopes
184
+ expires_at=refresh_token_expires_at,
185
+ )
186
+
187
+ self._access_to_refresh_map[access_token_value] = refresh_token_value
188
+ self._refresh_to_access_map[refresh_token_value] = access_token_value
189
+
190
+ return OAuthToken(
191
+ access_token=access_token_value,
192
+ token_type="bearer",
193
+ expires_in=DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS,
194
+ refresh_token=refresh_token_value,
195
+ scope=" ".join(authorization_code.scopes),
196
+ )
197
+
198
+ async def load_refresh_token(
199
+ self, client: OAuthClientInformationFull, refresh_token: str
200
+ ) -> RefreshToken | None:
201
+ token_obj = self.refresh_tokens.get(refresh_token)
202
+ if token_obj:
203
+ if token_obj.client_id != client.client_id:
204
+ return None # Belongs to different client
205
+ if token_obj.expires_at is not None and token_obj.expires_at < time.time():
206
+ self._revoke_internal(
207
+ refresh_token_str=token_obj.token
208
+ ) # Clean up expired
209
+ return None
210
+ return token_obj
211
+ return None
212
+
213
+ async def exchange_refresh_token(
214
+ self,
215
+ client: OAuthClientInformationFull,
216
+ refresh_token: RefreshToken, # This is the RefreshToken object, already loaded
217
+ scopes: list[str], # Requested scopes for the new access token
218
+ ) -> OAuthToken:
219
+ # Validate scopes: requested scopes must be a subset of original scopes
220
+ original_scopes = set(refresh_token.scopes)
221
+ requested_scopes = set(scopes)
222
+ if not requested_scopes.issubset(original_scopes):
223
+ raise TokenError(
224
+ "invalid_scope",
225
+ "Requested scopes exceed those authorized by the refresh token.",
226
+ )
227
+
228
+ # Invalidate old refresh token and its associated access token (rotation)
229
+ self._revoke_internal(refresh_token_str=refresh_token.token)
230
+
231
+ # Issue new tokens
232
+ new_access_token_value = f"test_access_token_{secrets.token_hex(32)}"
233
+ new_refresh_token_value = f"test_refresh_token_{secrets.token_hex(32)}"
234
+
235
+ access_token_expires_at = int(time.time() + DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS)
236
+
237
+ # Refresh token expiry
238
+ refresh_token_expires_at = None
239
+ if DEFAULT_REFRESH_TOKEN_EXPIRY_SECONDS is not None:
240
+ refresh_token_expires_at = int(
241
+ time.time() + DEFAULT_REFRESH_TOKEN_EXPIRY_SECONDS
242
+ )
243
+
244
+ self.access_tokens[new_access_token_value] = AccessToken(
245
+ token=new_access_token_value,
246
+ client_id=client.client_id,
247
+ scopes=scopes, # Use newly requested (and validated) scopes
248
+ expires_at=access_token_expires_at,
249
+ )
250
+ self.refresh_tokens[new_refresh_token_value] = RefreshToken(
251
+ token=new_refresh_token_value,
252
+ client_id=client.client_id,
253
+ scopes=scopes, # New refresh token also gets these scopes
254
+ expires_at=refresh_token_expires_at,
255
+ )
256
+
257
+ self._access_to_refresh_map[new_access_token_value] = new_refresh_token_value
258
+ self._refresh_to_access_map[new_refresh_token_value] = new_access_token_value
259
+
260
+ return OAuthToken(
261
+ access_token=new_access_token_value,
262
+ token_type="bearer",
263
+ expires_in=DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS,
264
+ refresh_token=new_refresh_token_value,
265
+ scope=" ".join(scopes),
266
+ )
267
+
268
+ async def load_access_token(self, token: str) -> AccessToken | None:
269
+ token_obj = self.access_tokens.get(token)
270
+ if token_obj:
271
+ if token_obj.expires_at is not None and token_obj.expires_at < time.time():
272
+ self._revoke_internal(
273
+ access_token_str=token_obj.token
274
+ ) # Clean up expired
275
+ return None
276
+ return token_obj
277
+ return None
278
+
279
+ def _revoke_internal(
280
+ self, access_token_str: str | None = None, refresh_token_str: str | None = None
281
+ ):
282
+ """Internal helper to remove tokens and their associations."""
283
+ removed_access_token = None
284
+ removed_refresh_token = None
285
+
286
+ if access_token_str:
287
+ if access_token_str in self.access_tokens:
288
+ del self.access_tokens[access_token_str]
289
+ removed_access_token = access_token_str
290
+
291
+ # Get associated refresh token
292
+ associated_refresh = self._access_to_refresh_map.pop(access_token_str, None)
293
+ if associated_refresh:
294
+ if associated_refresh in self.refresh_tokens:
295
+ del self.refresh_tokens[associated_refresh]
296
+ removed_refresh_token = associated_refresh
297
+ self._refresh_to_access_map.pop(associated_refresh, None)
298
+
299
+ if refresh_token_str:
300
+ if refresh_token_str in self.refresh_tokens:
301
+ del self.refresh_tokens[refresh_token_str]
302
+ removed_refresh_token = refresh_token_str
303
+
304
+ # Get associated access token
305
+ associated_access = self._refresh_to_access_map.pop(refresh_token_str, None)
306
+ if associated_access:
307
+ if associated_access in self.access_tokens:
308
+ del self.access_tokens[associated_access]
309
+ removed_access_token = associated_access
310
+ self._access_to_refresh_map.pop(associated_access, None)
311
+
312
+ # Clean up any dangling references if one part of the pair was already gone
313
+ if removed_access_token and removed_access_token in self._access_to_refresh_map:
314
+ del self._access_to_refresh_map[removed_access_token]
315
+ if (
316
+ removed_refresh_token
317
+ and removed_refresh_token in self._refresh_to_access_map
318
+ ):
319
+ del self._refresh_to_access_map[removed_refresh_token]
320
+
321
+ async def revoke_token(
322
+ self,
323
+ token: AccessToken | RefreshToken,
324
+ ) -> None:
325
+ """Revokes an access or refresh token and its counterpart."""
326
+ if isinstance(token, AccessToken):
327
+ self._revoke_internal(access_token_str=token.token)
328
+ elif isinstance(token, RefreshToken):
329
+ self._revoke_internal(refresh_token_str=token.token)
330
+ # 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,6 +128,7 @@ 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
134
  if settings:
@@ -186,13 +187,16 @@ class FastMCP(Generic[LifespanResultT]):
186
187
  lifespan=_lifespan_wrapper(self, lifespan),
187
188
  )
188
189
 
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
190
+ if auth is None and self.settings.default_auth_provider == "bearer_env":
191
+ auth = EnvBearerAuthProvider()
192
+ self.auth = auth
193
+
194
+ if tools:
195
+ for tool in tools:
196
+ if isinstance(tool, Tool):
197
+ self._tool_manager.add_tool(tool)
198
+ else:
199
+ self.add_tool(tool)
196
200
 
197
201
  # Set up MCP protocol handlers
198
202
  self._setup_handlers()
@@ -497,6 +501,7 @@ class FastMCP(Generic[LifespanResultT]):
497
501
  description: str | None = None,
498
502
  tags: set[str] | None = None,
499
503
  annotations: ToolAnnotations | dict[str, Any] | None = None,
504
+ exclude_args: list[str] | None = None,
500
505
  ) -> None:
501
506
  """Add a tool to the server.
502
507
 
@@ -519,6 +524,7 @@ class FastMCP(Generic[LifespanResultT]):
519
524
  description=description,
520
525
  tags=tags,
521
526
  annotations=annotations,
527
+ exclude_args=exclude_args,
522
528
  )
523
529
  self._cache.clear()
524
530
 
@@ -540,6 +546,7 @@ class FastMCP(Generic[LifespanResultT]):
540
546
  description: str | None = None,
541
547
  tags: set[str] | None = None,
542
548
  annotations: ToolAnnotations | dict[str, Any] | None = None,
549
+ exclude_args: list[str] | None = None,
543
550
  ) -> Callable[[AnyFunction], AnyFunction]:
544
551
  """Decorator to register a tool.
545
552
 
@@ -583,6 +590,7 @@ class FastMCP(Generic[LifespanResultT]):
583
590
  description=description,
584
591
  tags=tags,
585
592
  annotations=annotations,
593
+ exclude_args=exclude_args,
586
594
  )
587
595
  return fn
588
596
 
@@ -903,8 +911,7 @@ class FastMCP(Generic[LifespanResultT]):
903
911
  server=self,
904
912
  message_path=message_path or self.settings.message_path,
905
913
  sse_path=path or self.settings.sse_path,
906
- auth_server_provider=self._auth_server_provider,
907
- auth_settings=self.settings.auth,
914
+ auth=self.auth,
908
915
  debug=self.settings.debug,
909
916
  middleware=middleware,
910
917
  )
@@ -951,8 +958,7 @@ class FastMCP(Generic[LifespanResultT]):
951
958
  server=self,
952
959
  streamable_http_path=path or self.settings.streamable_http_path,
953
960
  event_store=None,
954
- auth_server_provider=self._auth_server_provider,
955
- auth_settings=self.settings.auth,
961
+ auth=self.auth,
956
962
  json_response=self.settings.json_response,
957
963
  stateless_http=self.settings.stateless_http,
958
964
  debug=self.settings.debug,
@@ -963,8 +969,7 @@ class FastMCP(Generic[LifespanResultT]):
963
969
  server=self,
964
970
  message_path=self.settings.message_path,
965
971
  sse_path=path or self.settings.sse_path,
966
- auth_server_provider=self._auth_server_provider,
967
- auth_settings=self.settings.auth,
972
+ auth=self.auth,
968
973
  debug=self.settings.debug,
969
974
  middleware=middleware,
970
975
  )