fastmcp 2.9.2__py3-none-any.whl → 2.10.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.
- fastmcp/client/auth/oauth.py +5 -82
- fastmcp/client/client.py +114 -24
- fastmcp/client/elicitation.py +63 -0
- fastmcp/client/transports.py +50 -36
- fastmcp/contrib/component_manager/README.md +170 -0
- fastmcp/contrib/component_manager/__init__.py +4 -0
- fastmcp/contrib/component_manager/component_manager.py +186 -0
- fastmcp/contrib/component_manager/component_service.py +225 -0
- fastmcp/contrib/component_manager/example.py +59 -0
- fastmcp/prompts/prompt.py +12 -4
- fastmcp/resources/resource.py +8 -3
- fastmcp/resources/template.py +5 -0
- fastmcp/server/auth/auth.py +15 -0
- fastmcp/server/auth/providers/bearer.py +41 -3
- fastmcp/server/auth/providers/bearer_env.py +4 -0
- fastmcp/server/auth/providers/in_memory.py +15 -0
- fastmcp/server/context.py +144 -4
- fastmcp/server/elicitation.py +160 -0
- fastmcp/server/http.py +1 -9
- fastmcp/server/low_level.py +4 -2
- fastmcp/server/middleware/__init__.py +14 -1
- fastmcp/server/middleware/logging.py +11 -0
- fastmcp/server/middleware/middleware.py +10 -6
- fastmcp/server/openapi.py +19 -77
- fastmcp/server/proxy.py +13 -6
- fastmcp/server/server.py +27 -7
- fastmcp/settings.py +0 -17
- fastmcp/tools/tool.py +209 -57
- fastmcp/tools/tool_manager.py +2 -3
- fastmcp/tools/tool_transform.py +125 -26
- fastmcp/utilities/components.py +5 -1
- fastmcp/utilities/json_schema_type.py +648 -0
- fastmcp/utilities/openapi.py +69 -0
- fastmcp/utilities/types.py +50 -19
- {fastmcp-2.9.2.dist-info → fastmcp-2.10.1.dist-info}/METADATA +3 -2
- {fastmcp-2.9.2.dist-info → fastmcp-2.10.1.dist-info}/RECORD +39 -31
- {fastmcp-2.9.2.dist-info → fastmcp-2.10.1.dist-info}/WHEEL +0 -0
- {fastmcp-2.9.2.dist-info → fastmcp-2.10.1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.9.2.dist-info → fastmcp-2.10.1.dist-info}/licenses/LICENSE +0 -0
fastmcp/prompts/prompt.py
CHANGED
|
@@ -9,9 +9,9 @@ from collections.abc import Awaitable, Callable, Sequence
|
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
11
|
import pydantic_core
|
|
12
|
+
from mcp.types import ContentBlock, PromptMessage, Role, TextContent
|
|
12
13
|
from mcp.types import Prompt as MCPPrompt
|
|
13
14
|
from mcp.types import PromptArgument as MCPPromptArgument
|
|
14
|
-
from mcp.types import PromptMessage, Role, TextContent
|
|
15
15
|
from pydantic import Field, TypeAdapter
|
|
16
16
|
|
|
17
17
|
from fastmcp.exceptions import PromptError
|
|
@@ -21,7 +21,6 @@ from fastmcp.utilities.json_schema import compress_schema
|
|
|
21
21
|
from fastmcp.utilities.logging import get_logger
|
|
22
22
|
from fastmcp.utilities.types import (
|
|
23
23
|
FastMCPBaseModel,
|
|
24
|
-
MCPContent,
|
|
25
24
|
find_kwarg_by_type,
|
|
26
25
|
get_cached_typeadapter,
|
|
27
26
|
)
|
|
@@ -30,7 +29,7 @@ logger = get_logger(__name__)
|
|
|
30
29
|
|
|
31
30
|
|
|
32
31
|
def Message(
|
|
33
|
-
content: str |
|
|
32
|
+
content: str | ContentBlock, role: Role | None = None, **kwargs: Any
|
|
34
33
|
) -> PromptMessage:
|
|
35
34
|
"""A user-friendly constructor for PromptMessage."""
|
|
36
35
|
if isinstance(content, str):
|
|
@@ -100,6 +99,7 @@ class Prompt(FastMCPComponent, ABC):
|
|
|
100
99
|
"name": self.name,
|
|
101
100
|
"description": self.description,
|
|
102
101
|
"arguments": arguments,
|
|
102
|
+
"title": self.title,
|
|
103
103
|
}
|
|
104
104
|
return MCPPrompt(**kwargs | overrides)
|
|
105
105
|
|
|
@@ -107,6 +107,7 @@ class Prompt(FastMCPComponent, ABC):
|
|
|
107
107
|
def from_function(
|
|
108
108
|
fn: Callable[..., PromptResult | Awaitable[PromptResult]],
|
|
109
109
|
name: str | None = None,
|
|
110
|
+
title: str | None = None,
|
|
110
111
|
description: str | None = None,
|
|
111
112
|
tags: set[str] | None = None,
|
|
112
113
|
enabled: bool | None = None,
|
|
@@ -120,7 +121,12 @@ class Prompt(FastMCPComponent, ABC):
|
|
|
120
121
|
- A sequence of any of the above
|
|
121
122
|
"""
|
|
122
123
|
return FunctionPrompt.from_function(
|
|
123
|
-
fn=fn,
|
|
124
|
+
fn=fn,
|
|
125
|
+
name=name,
|
|
126
|
+
title=title,
|
|
127
|
+
description=description,
|
|
128
|
+
tags=tags,
|
|
129
|
+
enabled=enabled,
|
|
124
130
|
)
|
|
125
131
|
|
|
126
132
|
@abstractmethod
|
|
@@ -142,6 +148,7 @@ class FunctionPrompt(Prompt):
|
|
|
142
148
|
cls,
|
|
143
149
|
fn: Callable[..., PromptResult | Awaitable[PromptResult]],
|
|
144
150
|
name: str | None = None,
|
|
151
|
+
title: str | None = None,
|
|
145
152
|
description: str | None = None,
|
|
146
153
|
tags: set[str] | None = None,
|
|
147
154
|
enabled: bool | None = None,
|
|
@@ -233,6 +240,7 @@ class FunctionPrompt(Prompt):
|
|
|
233
240
|
|
|
234
241
|
return cls(
|
|
235
242
|
name=func_name,
|
|
243
|
+
title=title,
|
|
236
244
|
description=description,
|
|
237
245
|
arguments=arguments,
|
|
238
246
|
tags=tags or set(),
|
fastmcp/resources/resource.py
CHANGED
|
@@ -62,9 +62,10 @@ class Resource(FastMCPComponent, abc.ABC):
|
|
|
62
62
|
|
|
63
63
|
@staticmethod
|
|
64
64
|
def from_function(
|
|
65
|
-
fn: Callable[
|
|
65
|
+
fn: Callable[..., Any],
|
|
66
66
|
uri: str | AnyUrl,
|
|
67
67
|
name: str | None = None,
|
|
68
|
+
title: str | None = None,
|
|
68
69
|
description: str | None = None,
|
|
69
70
|
mime_type: str | None = None,
|
|
70
71
|
tags: set[str] | None = None,
|
|
@@ -74,6 +75,7 @@ class Resource(FastMCPComponent, abc.ABC):
|
|
|
74
75
|
fn=fn,
|
|
75
76
|
uri=uri,
|
|
76
77
|
name=name,
|
|
78
|
+
title=title,
|
|
77
79
|
description=description,
|
|
78
80
|
mime_type=mime_type,
|
|
79
81
|
tags=tags,
|
|
@@ -111,6 +113,7 @@ class Resource(FastMCPComponent, abc.ABC):
|
|
|
111
113
|
"name": self.name,
|
|
112
114
|
"description": self.description,
|
|
113
115
|
"mimeType": self.mime_type,
|
|
116
|
+
"title": self.title,
|
|
114
117
|
}
|
|
115
118
|
return MCPResource(**kwargs | overrides)
|
|
116
119
|
|
|
@@ -141,14 +144,15 @@ class FunctionResource(Resource):
|
|
|
141
144
|
- other types will be converted to JSON
|
|
142
145
|
"""
|
|
143
146
|
|
|
144
|
-
fn: Callable[
|
|
147
|
+
fn: Callable[..., Any]
|
|
145
148
|
|
|
146
149
|
@classmethod
|
|
147
150
|
def from_function(
|
|
148
151
|
cls,
|
|
149
|
-
fn: Callable[
|
|
152
|
+
fn: Callable[..., Any],
|
|
150
153
|
uri: str | AnyUrl,
|
|
151
154
|
name: str | None = None,
|
|
155
|
+
title: str | None = None,
|
|
152
156
|
description: str | None = None,
|
|
153
157
|
mime_type: str | None = None,
|
|
154
158
|
tags: set[str] | None = None,
|
|
@@ -161,6 +165,7 @@ class FunctionResource(Resource):
|
|
|
161
165
|
fn=fn,
|
|
162
166
|
uri=uri,
|
|
163
167
|
name=name or fn.__name__,
|
|
168
|
+
title=title,
|
|
164
169
|
description=description or inspect.getdoc(fn),
|
|
165
170
|
mime_type=mime_type or "text/plain",
|
|
166
171
|
tags=tags or set(),
|
fastmcp/resources/template.py
CHANGED
|
@@ -86,6 +86,7 @@ class ResourceTemplate(FastMCPComponent):
|
|
|
86
86
|
fn: Callable[..., Any],
|
|
87
87
|
uri_template: str,
|
|
88
88
|
name: str | None = None,
|
|
89
|
+
title: str | None = None,
|
|
89
90
|
description: str | None = None,
|
|
90
91
|
mime_type: str | None = None,
|
|
91
92
|
tags: set[str] | None = None,
|
|
@@ -95,6 +96,7 @@ class ResourceTemplate(FastMCPComponent):
|
|
|
95
96
|
fn=fn,
|
|
96
97
|
uri_template=uri_template,
|
|
97
98
|
name=name,
|
|
99
|
+
title=title,
|
|
98
100
|
description=description,
|
|
99
101
|
mime_type=mime_type,
|
|
100
102
|
tags=tags,
|
|
@@ -144,6 +146,7 @@ class ResourceTemplate(FastMCPComponent):
|
|
|
144
146
|
"name": self.name,
|
|
145
147
|
"description": self.description,
|
|
146
148
|
"mimeType": self.mime_type,
|
|
149
|
+
"title": self.title,
|
|
147
150
|
}
|
|
148
151
|
return MCPResourceTemplate(**kwargs | overrides)
|
|
149
152
|
|
|
@@ -197,6 +200,7 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
|
197
200
|
fn: Callable[..., Any],
|
|
198
201
|
uri_template: str,
|
|
199
202
|
name: str | None = None,
|
|
203
|
+
title: str | None = None,
|
|
200
204
|
description: str | None = None,
|
|
201
205
|
mime_type: str | None = None,
|
|
202
206
|
tags: set[str] | None = None,
|
|
@@ -278,6 +282,7 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
|
278
282
|
return cls(
|
|
279
283
|
uri_template=uri_template,
|
|
280
284
|
name=func_name,
|
|
285
|
+
title=title,
|
|
281
286
|
description=description,
|
|
282
287
|
mime_type=mime_type or "text/plain",
|
|
283
288
|
fn=fn,
|
fastmcp/server/auth/auth.py
CHANGED
|
@@ -43,3 +43,18 @@ class OAuthProvider(
|
|
|
43
43
|
self.client_registration_options = client_registration_options
|
|
44
44
|
self.revocation_options = revocation_options
|
|
45
45
|
self.required_scopes = required_scopes
|
|
46
|
+
|
|
47
|
+
async def verify_token(self, token: str) -> AccessToken | None:
|
|
48
|
+
"""
|
|
49
|
+
Verify a bearer token and return access info if valid.
|
|
50
|
+
|
|
51
|
+
This method implements the TokenVerifier protocol by delegating
|
|
52
|
+
to our existing load_access_token method.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
token: The token string to validate
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
AccessToken object if valid, None if invalid or expired
|
|
59
|
+
"""
|
|
60
|
+
return await self.load_access_token(token)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import time
|
|
2
2
|
from dataclasses import dataclass
|
|
3
|
-
from typing import Any
|
|
3
|
+
from typing import Any
|
|
4
4
|
|
|
5
5
|
import httpx
|
|
6
6
|
from authlib.jose import JsonWebKey, JsonWebToken
|
|
@@ -18,6 +18,7 @@ from mcp.shared.auth import (
|
|
|
18
18
|
OAuthToken,
|
|
19
19
|
)
|
|
20
20
|
from pydantic import AnyHttpUrl, SecretStr, ValidationError
|
|
21
|
+
from typing_extensions import TypedDict
|
|
21
22
|
|
|
22
23
|
from fastmcp.server.auth.auth import (
|
|
23
24
|
ClientRegistrationOptions,
|
|
@@ -112,6 +113,7 @@ class RSAKeyPair:
|
|
|
112
113
|
Returns:
|
|
113
114
|
Signed JWT token string
|
|
114
115
|
"""
|
|
116
|
+
# TODO : Add support for configurable algorithms
|
|
115
117
|
jwt = JsonWebToken(["RS256"])
|
|
116
118
|
|
|
117
119
|
now = int(time.time())
|
|
@@ -150,7 +152,7 @@ class RSAKeyPair:
|
|
|
150
152
|
class BearerAuthProvider(OAuthProvider):
|
|
151
153
|
"""
|
|
152
154
|
Simple JWT Bearer Token validator for hosted MCP servers.
|
|
153
|
-
Uses RS256 asymmetric encryption. Supports either static public key
|
|
155
|
+
Uses RS256 asymmetric encryption by default but supports all JWA algorithms. Supports either static public key
|
|
154
156
|
or JWKS URI for key rotation.
|
|
155
157
|
|
|
156
158
|
Note that this provider DOES NOT permit client registration or revocation, or any OAuth flows.
|
|
@@ -162,6 +164,7 @@ class BearerAuthProvider(OAuthProvider):
|
|
|
162
164
|
public_key: str | None = None,
|
|
163
165
|
jwks_uri: str | None = None,
|
|
164
166
|
issuer: str | None = None,
|
|
167
|
+
algorithm: str | None = None,
|
|
165
168
|
audience: str | list[str] | None = None,
|
|
166
169
|
required_scopes: list[str] | None = None,
|
|
167
170
|
):
|
|
@@ -172,6 +175,7 @@ class BearerAuthProvider(OAuthProvider):
|
|
|
172
175
|
public_key: RSA public key in PEM format (for static key)
|
|
173
176
|
jwks_uri: URI to fetch keys from (for key rotation)
|
|
174
177
|
issuer: Expected issuer claim (optional)
|
|
178
|
+
algorithm: Algorithm to use for verification (optional, defaults to RS256)
|
|
175
179
|
audience: Expected audience claim - can be a string or list of strings (optional)
|
|
176
180
|
required_scopes: List of required scopes for access (optional)
|
|
177
181
|
"""
|
|
@@ -180,6 +184,24 @@ class BearerAuthProvider(OAuthProvider):
|
|
|
180
184
|
if public_key and jwks_uri:
|
|
181
185
|
raise ValueError("Provide either public_key or jwks_uri, not both")
|
|
182
186
|
|
|
187
|
+
if not algorithm:
|
|
188
|
+
algorithm = "RS256"
|
|
189
|
+
if algorithm not in {
|
|
190
|
+
"HS256",
|
|
191
|
+
"HS384",
|
|
192
|
+
"HS512",
|
|
193
|
+
"RS256",
|
|
194
|
+
"RS384",
|
|
195
|
+
"RS512",
|
|
196
|
+
"ES256",
|
|
197
|
+
"ES384",
|
|
198
|
+
"ES512",
|
|
199
|
+
"PS256",
|
|
200
|
+
"PS384",
|
|
201
|
+
"PS512",
|
|
202
|
+
}:
|
|
203
|
+
raise ValueError(f"Unsupported algorithm: {algorithm}.")
|
|
204
|
+
|
|
183
205
|
# Only pass issuer to parent if it's a valid URL, otherwise use default
|
|
184
206
|
# This allows the issuer claim validation to work with string issuers per RFC 7519
|
|
185
207
|
try:
|
|
@@ -195,11 +217,12 @@ class BearerAuthProvider(OAuthProvider):
|
|
|
195
217
|
required_scopes=required_scopes,
|
|
196
218
|
)
|
|
197
219
|
|
|
220
|
+
self.algorithm = algorithm
|
|
198
221
|
self.issuer = issuer
|
|
199
222
|
self.audience = audience
|
|
200
223
|
self.public_key = public_key
|
|
201
224
|
self.jwks_uri = jwks_uri
|
|
202
|
-
self.jwt = JsonWebToken([
|
|
225
|
+
self.jwt = JsonWebToken([self.algorithm]) # Use RS256 by default
|
|
203
226
|
self.logger = get_logger(__name__)
|
|
204
227
|
|
|
205
228
|
# Simple JWKS cache
|
|
@@ -384,6 +407,21 @@ class BearerAuthProvider(OAuthProvider):
|
|
|
384
407
|
return scope_claim
|
|
385
408
|
return []
|
|
386
409
|
|
|
410
|
+
async def verify_token(self, token: str) -> AccessToken | None:
|
|
411
|
+
"""
|
|
412
|
+
Verify a bearer token and return access info if valid.
|
|
413
|
+
|
|
414
|
+
This method implements the TokenVerifier protocol by delegating
|
|
415
|
+
to our existing load_access_token method.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
token: The JWT token string to validate
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
AccessToken object if valid, None if invalid or expired
|
|
422
|
+
"""
|
|
423
|
+
return await self.load_access_token(token)
|
|
424
|
+
|
|
387
425
|
# --- Unused OAuth server methods ---
|
|
388
426
|
async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:
|
|
389
427
|
raise NotImplementedError("Client management not supported")
|
|
@@ -17,6 +17,7 @@ class EnvBearerAuthProviderSettings(BaseSettings):
|
|
|
17
17
|
public_key: str | None = None
|
|
18
18
|
jwks_uri: str | None = None
|
|
19
19
|
issuer: str | None = None
|
|
20
|
+
algorithm: str | None = None
|
|
20
21
|
audience: str | None = None
|
|
21
22
|
required_scopes: list[str] | None = None
|
|
22
23
|
|
|
@@ -33,6 +34,7 @@ class EnvBearerAuthProvider(BearerAuthProvider):
|
|
|
33
34
|
public_key: str | None | EllipsisType = ...,
|
|
34
35
|
jwks_uri: str | None | EllipsisType = ...,
|
|
35
36
|
issuer: str | None | EllipsisType = ...,
|
|
37
|
+
algorithm: str | None | EllipsisType = ...,
|
|
36
38
|
audience: str | None | EllipsisType = ...,
|
|
37
39
|
required_scopes: list[str] | None | EllipsisType = ...,
|
|
38
40
|
):
|
|
@@ -43,6 +45,7 @@ class EnvBearerAuthProvider(BearerAuthProvider):
|
|
|
43
45
|
public_key: RSA public key in PEM format (for static key)
|
|
44
46
|
jwks_uri: URI to fetch keys from (for key rotation)
|
|
45
47
|
issuer: Expected issuer claim (optional)
|
|
48
|
+
algorithm: Algorithm to use for verification (optional)
|
|
46
49
|
audience: Expected audience claim (optional)
|
|
47
50
|
required_scopes: List of required scopes for access (optional)
|
|
48
51
|
"""
|
|
@@ -50,6 +53,7 @@ class EnvBearerAuthProvider(BearerAuthProvider):
|
|
|
50
53
|
"public_key": public_key,
|
|
51
54
|
"jwks_uri": jwks_uri,
|
|
52
55
|
"issuer": issuer,
|
|
56
|
+
"algorithm": algorithm,
|
|
53
57
|
"audience": audience,
|
|
54
58
|
"required_scopes": required_scopes,
|
|
55
59
|
}
|
|
@@ -271,6 +271,21 @@ class InMemoryOAuthProvider(OAuthProvider):
|
|
|
271
271
|
return token_obj
|
|
272
272
|
return None
|
|
273
273
|
|
|
274
|
+
async def verify_token(self, token: str) -> AccessToken | None:
|
|
275
|
+
"""
|
|
276
|
+
Verify a bearer token and return access info if valid.
|
|
277
|
+
|
|
278
|
+
This method implements the TokenVerifier protocol by delegating
|
|
279
|
+
to our existing load_access_token method.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
token: The token string to validate
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
AccessToken object if valid, None if invalid or expired
|
|
286
|
+
"""
|
|
287
|
+
return await self.load_access_token(token)
|
|
288
|
+
|
|
274
289
|
def _revoke_internal(
|
|
275
290
|
self, access_token_str: str | None = None, refresh_token_str: str | None = None
|
|
276
291
|
):
|
fastmcp/server/context.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
1
|
+
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import warnings
|
|
@@ -6,12 +6,15 @@ from collections.abc import Generator
|
|
|
6
6
|
from contextlib import contextmanager
|
|
7
7
|
from contextvars import ContextVar, Token
|
|
8
8
|
from dataclasses import dataclass
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Any, Literal, TypeVar, cast, get_origin, overload
|
|
9
11
|
|
|
10
12
|
from mcp import LoggingLevel, ServerSession
|
|
11
13
|
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
|
12
14
|
from mcp.server.lowlevel.server import request_ctx
|
|
13
15
|
from mcp.shared.context import RequestContext
|
|
14
16
|
from mcp.types import (
|
|
17
|
+
ContentBlock,
|
|
15
18
|
CreateMessageResult,
|
|
16
19
|
ModelHint,
|
|
17
20
|
ModelPreferences,
|
|
@@ -24,12 +27,20 @@ from starlette.requests import Request
|
|
|
24
27
|
|
|
25
28
|
import fastmcp.server.dependencies
|
|
26
29
|
from fastmcp import settings
|
|
30
|
+
from fastmcp.server.elicitation import (
|
|
31
|
+
AcceptedElicitation,
|
|
32
|
+
CancelledElicitation,
|
|
33
|
+
DeclinedElicitation,
|
|
34
|
+
ScalarElicitationType,
|
|
35
|
+
get_elicitation_schema,
|
|
36
|
+
)
|
|
27
37
|
from fastmcp.server.server import FastMCP
|
|
28
38
|
from fastmcp.utilities.logging import get_logger
|
|
29
|
-
from fastmcp.utilities.types import
|
|
39
|
+
from fastmcp.utilities.types import get_cached_typeadapter
|
|
30
40
|
|
|
31
41
|
logger = get_logger(__name__)
|
|
32
42
|
|
|
43
|
+
T = TypeVar("T")
|
|
33
44
|
_current_context: ContextVar[Context | None] = ContextVar("context", default=None)
|
|
34
45
|
_flush_lock = asyncio.Lock()
|
|
35
46
|
|
|
@@ -167,7 +178,10 @@ class Context:
|
|
|
167
178
|
if level is None:
|
|
168
179
|
level = "info"
|
|
169
180
|
await self.session.send_log_message(
|
|
170
|
-
level=level,
|
|
181
|
+
level=level,
|
|
182
|
+
data=message,
|
|
183
|
+
logger=logger_name,
|
|
184
|
+
related_request_id=self.request_id,
|
|
171
185
|
)
|
|
172
186
|
|
|
173
187
|
@property
|
|
@@ -261,7 +275,7 @@ class Context:
|
|
|
261
275
|
temperature: float | None = None,
|
|
262
276
|
max_tokens: int | None = None,
|
|
263
277
|
model_preferences: ModelPreferences | str | list[str] | None = None,
|
|
264
|
-
) ->
|
|
278
|
+
) -> ContentBlock:
|
|
265
279
|
"""
|
|
266
280
|
Send a sampling request to the client and await the response.
|
|
267
281
|
|
|
@@ -293,10 +307,136 @@ class Context:
|
|
|
293
307
|
temperature=temperature,
|
|
294
308
|
max_tokens=max_tokens,
|
|
295
309
|
model_preferences=self._parse_model_preferences(model_preferences),
|
|
310
|
+
related_request_id=self.request_id,
|
|
296
311
|
)
|
|
297
312
|
|
|
298
313
|
return result.content
|
|
299
314
|
|
|
315
|
+
@overload
|
|
316
|
+
async def elicit(
|
|
317
|
+
self,
|
|
318
|
+
message: str,
|
|
319
|
+
response_type: None,
|
|
320
|
+
) -> (
|
|
321
|
+
AcceptedElicitation[dict[str, Any]] | DeclinedElicitation | CancelledElicitation
|
|
322
|
+
): ...
|
|
323
|
+
|
|
324
|
+
"""When response_type is None, the accepted elicitaiton will contain an
|
|
325
|
+
empty dict"""
|
|
326
|
+
|
|
327
|
+
@overload
|
|
328
|
+
async def elicit(
|
|
329
|
+
self,
|
|
330
|
+
message: str,
|
|
331
|
+
response_type: type[T],
|
|
332
|
+
) -> AcceptedElicitation[T] | DeclinedElicitation | CancelledElicitation: ...
|
|
333
|
+
|
|
334
|
+
"""When response_type is not None, the accepted elicitaiton will contain the
|
|
335
|
+
response data"""
|
|
336
|
+
|
|
337
|
+
@overload
|
|
338
|
+
async def elicit(
|
|
339
|
+
self,
|
|
340
|
+
message: str,
|
|
341
|
+
response_type: list[str],
|
|
342
|
+
) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation: ...
|
|
343
|
+
|
|
344
|
+
"""When response_type is a list of strings, the accepted elicitaiton will
|
|
345
|
+
contain the selected string response"""
|
|
346
|
+
|
|
347
|
+
async def elicit(
|
|
348
|
+
self,
|
|
349
|
+
message: str,
|
|
350
|
+
response_type: type[T] | list[str] | None = None,
|
|
351
|
+
) -> (
|
|
352
|
+
AcceptedElicitation[T]
|
|
353
|
+
| AcceptedElicitation[dict[str, Any]]
|
|
354
|
+
| AcceptedElicitation[str]
|
|
355
|
+
| DeclinedElicitation
|
|
356
|
+
| CancelledElicitation
|
|
357
|
+
):
|
|
358
|
+
"""
|
|
359
|
+
Send an elicitation request to the client and await the response.
|
|
360
|
+
|
|
361
|
+
Call this method at any time to request additional information from
|
|
362
|
+
the user through the client. The client must support elicitation,
|
|
363
|
+
or the request will error.
|
|
364
|
+
|
|
365
|
+
Note that the MCP protocol only supports simple object schemas with
|
|
366
|
+
primitive types. You can provide a dataclass, TypedDict, or BaseModel to
|
|
367
|
+
comply. If you provide a primitive type, an object schema with a single
|
|
368
|
+
"value" field will be generated for the MCP interaction and
|
|
369
|
+
automatically deconstructed into the primitive type upon response.
|
|
370
|
+
|
|
371
|
+
If the response_type is None, the generated schema will be that of an
|
|
372
|
+
empty object in order to comply with the MCP protocol requirements.
|
|
373
|
+
Clients must send an empty object ("{}")in response.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
message: A human-readable message explaining what information is needed
|
|
377
|
+
response_type: The type of the response, which should be a primitive
|
|
378
|
+
type or dataclass or BaseModel. If it is a primitive type, an
|
|
379
|
+
object schema with a single "value" field will be generated.
|
|
380
|
+
"""
|
|
381
|
+
if response_type is None:
|
|
382
|
+
schema = {"type": "object", "properties": {}}
|
|
383
|
+
else:
|
|
384
|
+
# if the user provided a list of strings, treat it as a Literal
|
|
385
|
+
if isinstance(response_type, list):
|
|
386
|
+
if not all(isinstance(item, str) for item in response_type):
|
|
387
|
+
raise ValueError(
|
|
388
|
+
"List of options must be a list of strings. Received: "
|
|
389
|
+
f"{response_type}"
|
|
390
|
+
)
|
|
391
|
+
# Convert list of options to Literal type and wrap
|
|
392
|
+
choice_literal = Literal[tuple(response_type)] # type: ignore
|
|
393
|
+
response_type = ScalarElicitationType[choice_literal] # type: ignore
|
|
394
|
+
# if the user provided a primitive scalar, wrap it in an object schema
|
|
395
|
+
elif response_type in {bool, int, float, str}:
|
|
396
|
+
response_type = ScalarElicitationType[response_type] # type: ignore
|
|
397
|
+
# if the user provided a Literal type, wrap it in an object schema
|
|
398
|
+
elif get_origin(response_type) is Literal:
|
|
399
|
+
response_type = ScalarElicitationType[response_type] # type: ignore
|
|
400
|
+
# if the user provided an Enum type, wrap it in an object schema
|
|
401
|
+
elif isinstance(response_type, type) and issubclass(response_type, Enum):
|
|
402
|
+
response_type = ScalarElicitationType[response_type] # type: ignore
|
|
403
|
+
|
|
404
|
+
response_type = cast(type[T], response_type)
|
|
405
|
+
|
|
406
|
+
schema = get_elicitation_schema(response_type)
|
|
407
|
+
|
|
408
|
+
result = await self.session.elicit(
|
|
409
|
+
message=message,
|
|
410
|
+
requestedSchema=schema,
|
|
411
|
+
related_request_id=self.request_id,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
if result.action == "accept":
|
|
415
|
+
if response_type is not None:
|
|
416
|
+
type_adapter = get_cached_typeadapter(response_type)
|
|
417
|
+
validated_data = cast(
|
|
418
|
+
T | ScalarElicitationType[T],
|
|
419
|
+
type_adapter.validate_python(result.content),
|
|
420
|
+
)
|
|
421
|
+
if isinstance(validated_data, ScalarElicitationType):
|
|
422
|
+
return AcceptedElicitation[T](data=validated_data.value)
|
|
423
|
+
else:
|
|
424
|
+
return AcceptedElicitation[T](data=validated_data)
|
|
425
|
+
elif result.content:
|
|
426
|
+
raise ValueError(
|
|
427
|
+
"Elicitation expected an empty response, but received: "
|
|
428
|
+
f"{result.content}"
|
|
429
|
+
)
|
|
430
|
+
else:
|
|
431
|
+
return AcceptedElicitation[dict[str, Any]](data={})
|
|
432
|
+
elif result.action == "decline":
|
|
433
|
+
return DeclinedElicitation()
|
|
434
|
+
elif result.action == "cancel":
|
|
435
|
+
return CancelledElicitation()
|
|
436
|
+
else:
|
|
437
|
+
# This should never happen, but handle it just in case
|
|
438
|
+
raise ValueError(f"Unexpected elicitation action: {result.action}")
|
|
439
|
+
|
|
300
440
|
def get_http_request(self) -> Request:
|
|
301
441
|
"""Get the active starlette request."""
|
|
302
442
|
|