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.
- fastmcp/__init__.py +7 -2
- fastmcp/cli/cli.py +128 -33
- fastmcp/cli/install/__init__.py +2 -2
- fastmcp/cli/install/claude_code.py +42 -1
- fastmcp/cli/install/claude_desktop.py +42 -1
- fastmcp/cli/install/cursor.py +42 -1
- fastmcp/cli/install/{mcp_config.py → mcp_json.py} +51 -7
- fastmcp/cli/run.py +127 -1
- fastmcp/client/__init__.py +2 -0
- fastmcp/client/auth/oauth.py +68 -99
- fastmcp/client/oauth_callback.py +18 -0
- fastmcp/client/transports.py +69 -15
- fastmcp/contrib/component_manager/example.py +2 -2
- fastmcp/experimental/server/openapi/README.md +266 -0
- fastmcp/experimental/server/openapi/__init__.py +38 -0
- fastmcp/experimental/server/openapi/components.py +348 -0
- fastmcp/experimental/server/openapi/routing.py +132 -0
- fastmcp/experimental/server/openapi/server.py +466 -0
- fastmcp/experimental/utilities/openapi/README.md +239 -0
- fastmcp/experimental/utilities/openapi/__init__.py +68 -0
- fastmcp/experimental/utilities/openapi/director.py +208 -0
- fastmcp/experimental/utilities/openapi/formatters.py +355 -0
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +340 -0
- fastmcp/experimental/utilities/openapi/models.py +85 -0
- fastmcp/experimental/utilities/openapi/parser.py +618 -0
- fastmcp/experimental/utilities/openapi/schemas.py +538 -0
- fastmcp/mcp_config.py +125 -88
- fastmcp/prompts/prompt.py +11 -1
- fastmcp/prompts/prompt_manager.py +1 -1
- fastmcp/resources/resource.py +21 -1
- fastmcp/resources/resource_manager.py +2 -2
- fastmcp/resources/template.py +20 -1
- fastmcp/server/auth/__init__.py +17 -2
- fastmcp/server/auth/auth.py +144 -7
- fastmcp/server/auth/providers/bearer.py +25 -473
- fastmcp/server/auth/providers/in_memory.py +4 -2
- fastmcp/server/auth/providers/jwt.py +538 -0
- fastmcp/server/auth/providers/workos.py +170 -0
- fastmcp/server/auth/registry.py +52 -0
- fastmcp/server/context.py +110 -26
- fastmcp/server/dependencies.py +9 -2
- fastmcp/server/http.py +62 -30
- fastmcp/server/middleware/middleware.py +3 -23
- fastmcp/server/openapi.py +26 -13
- fastmcp/server/proxy.py +89 -8
- fastmcp/server/server.py +170 -62
- fastmcp/settings.py +83 -18
- fastmcp/tools/tool.py +41 -6
- fastmcp/tools/tool_manager.py +39 -3
- fastmcp/tools/tool_transform.py +122 -6
- fastmcp/utilities/components.py +35 -2
- fastmcp/utilities/json_schema.py +136 -98
- fastmcp/utilities/json_schema_type.py +1 -3
- fastmcp/utilities/mcp_config.py +28 -0
- fastmcp/utilities/openapi.py +306 -30
- fastmcp/utilities/tests.py +54 -6
- fastmcp/utilities/types.py +89 -11
- {fastmcp-2.10.5.dist-info → fastmcp-2.11.0.dist-info}/METADATA +4 -3
- fastmcp-2.11.0.dist-info/RECORD +108 -0
- fastmcp/server/auth/providers/bearer_env.py +0 -63
- fastmcp/utilities/cache.py +0 -26
- fastmcp-2.10.5.dist-info/RECORD +0 -93
- {fastmcp-2.10.5.dist-info → fastmcp-2.11.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.10.5.dist-info → fastmcp-2.11.0.dist-info}/entry_points.txt +0 -0
- {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=
|
|
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
|
|
203
|
-
"""Get the MCP session ID for
|
|
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
|
|
211
|
-
for
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
224
|
-
|
|
255
|
+
request_ctx = self.request_context
|
|
256
|
+
session = request_ctx.session
|
|
225
257
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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(
|
|
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(
|
|
291
|
+
await self.log(
|
|
292
|
+
level="debug", message=message, logger_name=logger_name, extra=extra
|
|
293
|
+
)
|
|
241
294
|
|
|
242
|
-
async def info(
|
|
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(
|
|
302
|
+
await self.log(
|
|
303
|
+
level="info", message=message, logger_name=logger_name, extra=extra
|
|
304
|
+
)
|
|
245
305
|
|
|
246
|
-
async def warning(
|
|
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(
|
|
313
|
+
await self.log(
|
|
314
|
+
level="warning", message=message, logger_name=logger_name, extra=extra
|
|
315
|
+
)
|
|
249
316
|
|
|
250
|
-
async def error(
|
|
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(
|
|
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")
|
fastmcp/server/dependencies.py
CHANGED
|
@@ -37,9 +37,14 @@ def get_context() -> Context:
|
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
def get_http_request() -> Request:
|
|
40
|
-
from
|
|
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):
|