fastmcp 2.10.5__py3-none-any.whl → 2.11.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.
Files changed (65) hide show
  1. fastmcp/__init__.py +7 -2
  2. fastmcp/cli/cli.py +128 -33
  3. fastmcp/cli/install/__init__.py +2 -2
  4. fastmcp/cli/install/claude_code.py +42 -1
  5. fastmcp/cli/install/claude_desktop.py +42 -1
  6. fastmcp/cli/install/cursor.py +42 -1
  7. fastmcp/cli/install/{mcp_config.py → mcp_json.py} +51 -7
  8. fastmcp/cli/run.py +127 -1
  9. fastmcp/client/__init__.py +2 -0
  10. fastmcp/client/auth/oauth.py +68 -99
  11. fastmcp/client/oauth_callback.py +18 -0
  12. fastmcp/client/transports.py +69 -15
  13. fastmcp/contrib/component_manager/example.py +2 -2
  14. fastmcp/experimental/server/openapi/README.md +266 -0
  15. fastmcp/experimental/server/openapi/__init__.py +38 -0
  16. fastmcp/experimental/server/openapi/components.py +348 -0
  17. fastmcp/experimental/server/openapi/routing.py +132 -0
  18. fastmcp/experimental/server/openapi/server.py +466 -0
  19. fastmcp/experimental/utilities/openapi/README.md +239 -0
  20. fastmcp/experimental/utilities/openapi/__init__.py +68 -0
  21. fastmcp/experimental/utilities/openapi/director.py +208 -0
  22. fastmcp/experimental/utilities/openapi/formatters.py +355 -0
  23. fastmcp/experimental/utilities/openapi/json_schema_converter.py +340 -0
  24. fastmcp/experimental/utilities/openapi/models.py +85 -0
  25. fastmcp/experimental/utilities/openapi/parser.py +618 -0
  26. fastmcp/experimental/utilities/openapi/schemas.py +538 -0
  27. fastmcp/mcp_config.py +125 -88
  28. fastmcp/prompts/prompt.py +11 -1
  29. fastmcp/prompts/prompt_manager.py +1 -1
  30. fastmcp/resources/resource.py +21 -1
  31. fastmcp/resources/resource_manager.py +2 -2
  32. fastmcp/resources/template.py +20 -1
  33. fastmcp/server/auth/__init__.py +17 -2
  34. fastmcp/server/auth/auth.py +144 -7
  35. fastmcp/server/auth/providers/bearer.py +25 -473
  36. fastmcp/server/auth/providers/in_memory.py +4 -2
  37. fastmcp/server/auth/providers/jwt.py +538 -0
  38. fastmcp/server/auth/providers/workos.py +170 -0
  39. fastmcp/server/auth/registry.py +52 -0
  40. fastmcp/server/context.py +110 -26
  41. fastmcp/server/dependencies.py +9 -2
  42. fastmcp/server/http.py +62 -30
  43. fastmcp/server/middleware/middleware.py +3 -23
  44. fastmcp/server/openapi.py +26 -13
  45. fastmcp/server/proxy.py +89 -8
  46. fastmcp/server/server.py +170 -62
  47. fastmcp/settings.py +83 -18
  48. fastmcp/tools/tool.py +41 -6
  49. fastmcp/tools/tool_manager.py +39 -3
  50. fastmcp/tools/tool_transform.py +122 -6
  51. fastmcp/utilities/components.py +35 -2
  52. fastmcp/utilities/json_schema.py +136 -98
  53. fastmcp/utilities/json_schema_type.py +1 -3
  54. fastmcp/utilities/mcp_config.py +28 -0
  55. fastmcp/utilities/openapi.py +306 -30
  56. fastmcp/utilities/tests.py +54 -6
  57. fastmcp/utilities/types.py +89 -11
  58. {fastmcp-2.10.5.dist-info → fastmcp-2.11.0.dist-info}/METADATA +4 -3
  59. fastmcp-2.11.0.dist-info/RECORD +108 -0
  60. fastmcp/server/auth/providers/bearer_env.py +0 -63
  61. fastmcp/utilities/cache.py +0 -26
  62. fastmcp-2.10.5.dist-info/RECORD +0 -93
  63. {fastmcp-2.10.5.dist-info → fastmcp-2.11.0.dist-info}/WHEEL +0 -0
  64. {fastmcp-2.10.5.dist-info → fastmcp-2.11.0.dist-info}/entry_points.txt +0 -0
  65. {fastmcp-2.10.5.dist-info → fastmcp-2.11.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,170 @@
1
+ from __future__ import annotations
2
+
3
+ import httpx
4
+ from mcp.server.auth.provider import (
5
+ AccessToken,
6
+ )
7
+ from pydantic import AnyHttpUrl
8
+ from pydantic_settings import BaseSettings, SettingsConfigDict
9
+ from starlette.responses import JSONResponse
10
+ from starlette.routing import BaseRoute, Route
11
+
12
+ from fastmcp.server.auth.auth import AuthProvider, TokenVerifier
13
+ from fastmcp.server.auth.providers.jwt import JWTVerifier
14
+ from fastmcp.server.auth.registry import register_provider
15
+ from fastmcp.utilities.logging import get_logger
16
+ from fastmcp.utilities.types import NotSet, NotSetT
17
+
18
+ logger = get_logger(__name__)
19
+
20
+
21
+ class AuthKitProviderSettings(BaseSettings):
22
+ model_config = SettingsConfigDict(
23
+ env_prefix="FASTMCP_SERVER_AUTH_AUTHKITPROVIDER_",
24
+ env_file=".env",
25
+ extra="ignore",
26
+ )
27
+
28
+ authkit_domain: AnyHttpUrl
29
+ base_url: AnyHttpUrl
30
+ required_scopes: list[str] | None = None
31
+
32
+
33
+ @register_provider("AUTHKIT")
34
+ class AuthKitProvider(AuthProvider):
35
+ """WorkOS AuthKit metadata provider for DCR (Dynamic Client Registration).
36
+
37
+ This provider implements WorkOS AuthKit integration using metadata forwarding
38
+ instead of OAuth proxying. This is the recommended approach for WorkOS DCR
39
+ as it allows WorkOS to handle the OAuth flow directly while FastMCP acts
40
+ as a resource server.
41
+
42
+ IMPORTANT SETUP REQUIREMENTS:
43
+
44
+ 1. Enable Dynamic Client Registration in WorkOS Dashboard:
45
+ - Go to Applications → Configuration
46
+ - Toggle "Dynamic Client Registration" to enabled
47
+
48
+ 2. Configure your FastMCP server URL as a callback:
49
+ - Add your server URL to the Redirects tab in WorkOS dashboard
50
+ - Example: https://your-fastmcp-server.com/oauth2/callback
51
+
52
+ For detailed setup instructions, see:
53
+ https://workos.com/docs/authkit/mcp/integrating/token-verification
54
+
55
+ Example:
56
+ ```python
57
+ from fastmcp.server.auth.providers.workos import AuthKitProvider
58
+
59
+ # Create WorkOS metadata provider (JWT verifier created automatically)
60
+ workos_auth = AuthKitProvider(
61
+ authkit_domain="https://your-workos-domain.authkit.app",
62
+ base_url="https://your-fastmcp-server.com",
63
+ )
64
+
65
+ # Use with FastMCP
66
+ mcp = FastMCP("My App", auth=workos_auth)
67
+ ```
68
+ """
69
+
70
+ def __init__(
71
+ self,
72
+ *,
73
+ authkit_domain: AnyHttpUrl | str | NotSetT = NotSet,
74
+ base_url: AnyHttpUrl | str | NotSetT = NotSet,
75
+ required_scopes: list[str] | None | NotSetT = NotSet,
76
+ token_verifier: TokenVerifier | None = None,
77
+ ):
78
+ """Initialize WorkOS metadata provider.
79
+
80
+ Args:
81
+ authkit_domain: Your WorkOS AuthKit domain (e.g., "https://your-app.authkit.app")
82
+ base_url: Public URL of this FastMCP server
83
+ required_scopes: Optional list of scopes to require for all requests
84
+ token_verifier: Optional token verifier. If None, creates JWT verifier for WorkOS
85
+ """
86
+ super().__init__()
87
+
88
+ settings = AuthKitProviderSettings.model_validate(
89
+ {
90
+ k: v
91
+ for k, v in {
92
+ "authkit_domain": authkit_domain,
93
+ "base_url": base_url,
94
+ "required_scopes": required_scopes,
95
+ }.items()
96
+ if v is not NotSet
97
+ }
98
+ )
99
+
100
+ self.authkit_domain = str(settings.authkit_domain).rstrip("/")
101
+ self.base_url = str(settings.base_url).rstrip("/")
102
+
103
+ # Create default JWT verifier if none provided
104
+ if token_verifier is None:
105
+ token_verifier = JWTVerifier(
106
+ jwks_uri=f"{self.authkit_domain}/oauth2/jwks",
107
+ issuer=self.authkit_domain,
108
+ algorithm="RS256",
109
+ required_scopes=settings.required_scopes,
110
+ )
111
+
112
+ self.token_verifier = token_verifier
113
+
114
+ async def verify_token(self, token: str) -> AccessToken | None:
115
+ """Verify a WorkOS token using the configured token verifier."""
116
+ return await self.token_verifier.verify_token(token)
117
+
118
+ def customize_auth_routes(self, routes: list[BaseRoute]) -> list[BaseRoute]:
119
+ """Add AuthKit metadata endpoints.
120
+
121
+ This adds:
122
+ - /.well-known/oauth-authorization-server (forwards AuthKit metadata)
123
+ - /.well-known/oauth-protected-resource (returns FastMCP resource info)
124
+ """
125
+
126
+ async def oauth_authorization_server_metadata(request):
127
+ """Forward AuthKit OAuth authorization server metadata with FastMCP customizations."""
128
+ try:
129
+ async with httpx.AsyncClient() as client:
130
+ response = await client.get(
131
+ f"{self.authkit_domain}/.well-known/oauth-authorization-server"
132
+ )
133
+ response.raise_for_status()
134
+ metadata = response.json()
135
+ return JSONResponse(metadata)
136
+ except Exception as e:
137
+ return JSONResponse(
138
+ {
139
+ "error": "server_error",
140
+ "error_description": f"Failed to fetch AuthKit metadata: {e}",
141
+ },
142
+ status_code=500,
143
+ )
144
+
145
+ async def oauth_protected_resource_metadata(request):
146
+ """Return FastMCP resource server metadata."""
147
+ return JSONResponse(
148
+ {
149
+ "resource": self.base_url,
150
+ "authorization_servers": [self.authkit_domain],
151
+ "bearer_methods_supported": ["header"],
152
+ }
153
+ )
154
+
155
+ routes.extend(
156
+ [
157
+ Route(
158
+ "/.well-known/oauth-authorization-server",
159
+ endpoint=oauth_authorization_server_metadata,
160
+ methods=["GET"],
161
+ ),
162
+ Route(
163
+ "/.well-known/oauth-protected-resource",
164
+ endpoint=oauth_protected_resource_metadata,
165
+ methods=["GET"],
166
+ ),
167
+ ]
168
+ )
169
+
170
+ return routes
@@ -0,0 +1,52 @@
1
+ """Provider registry for FastMCP auth providers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from typing import TYPE_CHECKING, TypeVar
7
+
8
+ if TYPE_CHECKING:
9
+ from fastmcp.server.auth.auth import AuthProvider
10
+
11
+ # Type variable for auth providers
12
+ T = TypeVar("T", bound="AuthProvider")
13
+
14
+
15
+ # Provider Registry
16
+ _PROVIDER_REGISTRY: dict[str, type[AuthProvider]] = {}
17
+
18
+
19
+ def register_provider(name: str) -> Callable[[type[T]], type[T]]:
20
+ """Decorator to register an auth provider with a given name.
21
+
22
+ Args:
23
+ name: The name to register the provider under (e.g., 'AUTHKIT')
24
+
25
+ Returns:
26
+ The decorated class
27
+
28
+ Example:
29
+ @register_provider('AUTHKIT')
30
+ class AuthKitProvider(AuthProvider):
31
+ ...
32
+ """
33
+
34
+ def decorator(cls: type[T]) -> type[T]:
35
+ _PROVIDER_REGISTRY[name.upper()] = cls
36
+ return cls
37
+
38
+ return decorator
39
+
40
+
41
+ def get_registered_provider(name: str) -> type[AuthProvider]:
42
+ """Get a registered provider by name.
43
+
44
+ Args:
45
+ name: The provider name (case-insensitive)
46
+
47
+ Returns:
48
+ The provider class if found, None otherwise
49
+ """
50
+ if name.upper() in _PROVIDER_REGISTRY:
51
+ return _PROVIDER_REGISTRY[name.upper()]
52
+ raise ValueError(f"Provider {name!r} has not been registered.")
fastmcp/server/context.py CHANGED
@@ -1,8 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
+ import copy
4
5
  import warnings
5
- from collections.abc import Generator
6
+ from collections.abc import Generator, Mapping
6
7
  from contextlib import contextmanager
7
8
  from contextvars import ContextVar, Token
8
9
  from dataclasses import dataclass
@@ -16,6 +17,7 @@ from mcp.shared.context import RequestContext
16
17
  from mcp.types import (
17
18
  ContentBlock,
18
19
  CreateMessageResult,
20
+ IncludeContext,
19
21
  ModelHint,
20
22
  ModelPreferences,
21
23
  Root,
@@ -45,6 +47,18 @@ _current_context: ContextVar[Context | None] = ContextVar("context", default=Non
45
47
  _flush_lock = asyncio.Lock()
46
48
 
47
49
 
50
+ @dataclass
51
+ class LogData:
52
+ """Data object for passing log arguments to client-side handlers.
53
+
54
+ This provides an interface to match the Python standard library logging,
55
+ for compatibility with structured logging.
56
+ """
57
+
58
+ msg: str
59
+ extra: Mapping[str, Any] | None = None
60
+
61
+
48
62
  @contextmanager
49
63
  def set_context(context: Context) -> Generator[Context, None, None]:
50
64
  token = _current_context.set(context)
@@ -82,9 +96,19 @@ class Context:
82
96
  request_id = ctx.request_id
83
97
  client_id = ctx.client_id
84
98
 
99
+ # Manage state across the request
100
+ ctx.set_state("key", "value")
101
+ value = ctx.get_state("key")
102
+
85
103
  return str(x)
86
104
  ```
87
105
 
106
+ State Management:
107
+ Context objects maintain a state dictionary that can be used to store and share
108
+ data across middleware and tool calls within a request. When a new context
109
+ is created (nested contexts), it inherits a copy of its parent's state, ensuring
110
+ that modifications in child contexts don't affect parent contexts.
111
+
88
112
  The context parameter name can be anything as long as it's annotated with Context.
89
113
  The context is optional - tools that don't need it can omit the parameter.
90
114
 
@@ -94,9 +118,15 @@ class Context:
94
118
  self.fastmcp = fastmcp
95
119
  self._tokens: list[Token] = []
96
120
  self._notification_queue: set[str] = set() # Dedupe notifications
121
+ self._state: dict[str, Any] = {}
97
122
 
98
123
  async def __aenter__(self) -> Context:
99
124
  """Enter the context manager and set this context as the current context."""
125
+ parent_context = _current_context.get(None)
126
+ if parent_context is not None:
127
+ # Inherit state from parent context
128
+ self._state = copy.deepcopy(parent_context._state)
129
+
100
130
  # Always set this context and save the token
101
131
  token = _current_context.set(self)
102
132
  self._tokens.append(token)
@@ -112,7 +142,7 @@ class Context:
112
142
  _current_context.reset(token)
113
143
 
114
144
  @property
115
- def request_context(self) -> RequestContext:
145
+ def request_context(self) -> RequestContext[ServerSession, Any, Request]:
116
146
  """Access to the underlying request context.
117
147
 
118
148
  If called outside of a request context, this will raise a ValueError.
@@ -166,6 +196,7 @@ class Context:
166
196
  message: str,
167
197
  level: LoggingLevel | None = None,
168
198
  logger_name: str | None = None,
199
+ extra: Mapping[str, Any] | None = None,
169
200
  ) -> None:
170
201
  """Send a log message to the client.
171
202
 
@@ -174,12 +205,14 @@ class Context:
174
205
  level: Optional log level. One of "debug", "info", "notice", "warning", "error", "critical",
175
206
  "alert", or "emergency". Default is "info".
176
207
  logger_name: Optional logger name
208
+ extra: Optional mapping for additional arguments
177
209
  """
178
210
  if level is None:
179
211
  level = "info"
212
+ data = LogData(msg=message, extra=extra)
180
213
  await self.session.send_log_message(
181
214
  level=level,
182
- data=message,
215
+ data=data,
183
216
  logger=logger_name,
184
217
  related_request_id=self.request_id,
185
218
  )
@@ -199,35 +232,48 @@ class Context:
199
232
  return str(self.request_context.request_id)
200
233
 
201
234
  @property
202
- def session_id(self) -> str | None:
203
- """Get the MCP session ID for HTTP transports.
235
+ def session_id(self) -> str:
236
+ """Get the MCP session ID for ALL transports.
204
237
 
205
238
  Returns the session ID that can be used as a key for session-based
206
239
  data storage (e.g., Redis) to share data between tool calls within
207
240
  the same client session.
208
241
 
209
242
  Returns:
210
- The session ID for HTTP transports (SSE, StreamableHTTP), or None
211
- for stdio and in-memory transports which don't use session IDs.
243
+ The session ID for StreamableHTTP transports, or a generated ID
244
+ for other transports.
212
245
 
213
246
  Example:
214
247
  ```python
215
248
  @server.tool
216
249
  def store_data(data: dict, ctx: Context) -> str:
217
- if session_id := ctx.session_id:
218
- redis_client.set(f"session:{session_id}:data", json.dumps(data))
219
- return f"Data stored for session {session_id}"
220
- return "No session ID available (stdio/memory transport)"
250
+ session_id = ctx.session_id
251
+ redis_client.set(f"session:{session_id}:data", json.dumps(data))
252
+ return f"Data stored for session {session_id}"
221
253
  ```
222
254
  """
223
- try:
224
- from fastmcp.server.dependencies import get_http_headers
255
+ request_ctx = self.request_context
256
+ session = request_ctx.session
225
257
 
226
- headers = get_http_headers(include_all=True)
227
- return headers.get("mcp-session-id")
228
- except RuntimeError:
229
- # No HTTP context available (stdio/in-memory transport)
230
- return None
258
+ # Try to get the session ID from the session attributes
259
+ session_id = getattr(session, "_fastmcp_id", None)
260
+ if session_id is not None:
261
+ return session_id
262
+
263
+ # Try to get the session ID from the http request headers
264
+ request = request_ctx.request
265
+ if request:
266
+ session_id = request.headers.get("mcp-session-id")
267
+
268
+ # Generate a session ID if it doesn't exist.
269
+ if session_id is None:
270
+ from uuid import uuid4
271
+
272
+ session_id = str(uuid4())
273
+
274
+ # Save the session id to the session attributes
275
+ setattr(session, "_fastmcp_id", session_id)
276
+ return session_id
231
277
 
232
278
  @property
233
279
  def session(self) -> ServerSession:
@@ -235,21 +281,49 @@ class Context:
235
281
  return self.request_context.session
236
282
 
237
283
  # Convenience methods for common log levels
238
- async def debug(self, message: str, logger_name: str | None = None) -> None:
284
+ async def debug(
285
+ self,
286
+ message: str,
287
+ logger_name: str | None = None,
288
+ extra: Mapping[str, Any] | None = None,
289
+ ) -> None:
239
290
  """Send a debug log message."""
240
- await self.log(level="debug", message=message, logger_name=logger_name)
291
+ await self.log(
292
+ level="debug", message=message, logger_name=logger_name, extra=extra
293
+ )
241
294
 
242
- async def info(self, message: str, logger_name: str | None = None) -> None:
295
+ async def info(
296
+ self,
297
+ message: str,
298
+ logger_name: str | None = None,
299
+ extra: Mapping[str, Any] | None = None,
300
+ ) -> None:
243
301
  """Send an info log message."""
244
- await self.log(level="info", message=message, logger_name=logger_name)
302
+ await self.log(
303
+ level="info", message=message, logger_name=logger_name, extra=extra
304
+ )
245
305
 
246
- async def warning(self, message: str, logger_name: str | None = None) -> None:
306
+ async def warning(
307
+ self,
308
+ message: str,
309
+ logger_name: str | None = None,
310
+ extra: Mapping[str, Any] | None = None,
311
+ ) -> None:
247
312
  """Send a warning log message."""
248
- await self.log(level="warning", message=message, logger_name=logger_name)
313
+ await self.log(
314
+ level="warning", message=message, logger_name=logger_name, extra=extra
315
+ )
249
316
 
250
- async def error(self, message: str, logger_name: str | None = None) -> None:
317
+ async def error(
318
+ self,
319
+ message: str,
320
+ logger_name: str | None = None,
321
+ extra: Mapping[str, Any] | None = None,
322
+ ) -> None:
251
323
  """Send an error log message."""
252
- await self.log(level="error", message=message, logger_name=logger_name)
324
+ await self.log(
325
+ level="error", message=message, logger_name=logger_name, extra=extra
326
+ )
253
327
 
254
328
  async def list_roots(self) -> list[Root]:
255
329
  """List the roots available to the server, as indicated by the client."""
@@ -272,6 +346,7 @@ class Context:
272
346
  self,
273
347
  messages: str | list[str | SamplingMessage],
274
348
  system_prompt: str | None = None,
349
+ include_context: IncludeContext | None = None,
275
350
  temperature: float | None = None,
276
351
  max_tokens: int | None = None,
277
352
  model_preferences: ModelPreferences | str | list[str] | None = None,
@@ -304,6 +379,7 @@ class Context:
304
379
  result: CreateMessageResult = await self.session.create_message(
305
380
  messages=sampling_messages,
306
381
  system_prompt=system_prompt,
382
+ include_context=include_context,
307
383
  temperature=temperature,
308
384
  max_tokens=max_tokens,
309
385
  model_preferences=self._parse_model_preferences(model_preferences),
@@ -452,6 +528,14 @@ class Context:
452
528
 
453
529
  return fastmcp.server.dependencies.get_http_request()
454
530
 
531
+ def set_state(self, key: str, value: Any) -> None:
532
+ """Set a value in the context state."""
533
+ self._state[key] = value
534
+
535
+ def get_state(self, key: str) -> Any:
536
+ """Get a value from the context state. Returns None if the key is not found."""
537
+ return self._state.get(key)
538
+
455
539
  def _queue_tool_list_changed(self) -> None:
456
540
  """Queue a tool list changed notification."""
457
541
  self._notification_queue.add("notifications/tools/list_changed")
@@ -37,9 +37,14 @@ def get_context() -> Context:
37
37
 
38
38
 
39
39
  def get_http_request() -> Request:
40
- from fastmcp.server.http import _current_http_request
40
+ from mcp.server.lowlevel.server import request_ctx
41
+
42
+ request = None
43
+ try:
44
+ request = request_ctx.get().request
45
+ except LookupError:
46
+ pass
41
47
 
42
- request = _current_http_request.get()
43
48
  if request is None:
44
49
  raise RuntimeError("No active HTTP request found.")
45
50
  return request
@@ -72,6 +77,8 @@ def get_http_headers(include_all: bool = False) -> dict[str, str]:
72
77
  "proxy-authenticate",
73
78
  "proxy-authorization",
74
79
  "proxy-connection",
80
+ # MCP-related headers
81
+ "mcp-session-id",
75
82
  }
76
83
  # (just in case)
77
84
  if not all(h.lower() == h for h in exclude_headers):