fastmcp 2.12.5__py3-none-any.whl → 2.14.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 +2 -23
- fastmcp/cli/__init__.py +0 -3
- fastmcp/cli/__main__.py +5 -0
- fastmcp/cli/cli.py +19 -33
- fastmcp/cli/install/claude_code.py +6 -6
- fastmcp/cli/install/claude_desktop.py +3 -3
- fastmcp/cli/install/cursor.py +18 -12
- fastmcp/cli/install/gemini_cli.py +3 -3
- fastmcp/cli/install/mcp_json.py +3 -3
- fastmcp/cli/install/shared.py +0 -15
- fastmcp/cli/run.py +13 -8
- fastmcp/cli/tasks.py +110 -0
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +123 -225
- fastmcp/client/client.py +697 -95
- fastmcp/client/elicitation.py +11 -5
- fastmcp/client/logging.py +18 -14
- fastmcp/client/messages.py +7 -5
- fastmcp/client/oauth_callback.py +85 -171
- fastmcp/client/roots.py +2 -1
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/tasks.py +614 -0
- fastmcp/client/transports.py +117 -30
- fastmcp/contrib/component_manager/__init__.py +1 -1
- fastmcp/contrib/component_manager/component_manager.py +2 -2
- fastmcp/contrib/component_manager/component_service.py +10 -26
- fastmcp/contrib/mcp_mixin/README.md +32 -1
- fastmcp/contrib/mcp_mixin/__init__.py +2 -2
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
- fastmcp/dependencies.py +25 -0
- fastmcp/experimental/sampling/handlers/openai.py +3 -3
- fastmcp/experimental/server/openapi/__init__.py +20 -21
- fastmcp/experimental/utilities/openapi/__init__.py +16 -47
- fastmcp/mcp_config.py +3 -4
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/prompt.py +54 -51
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/resource.py +43 -21
- fastmcp/resources/resource_manager.py +9 -168
- fastmcp/resources/template.py +161 -61
- fastmcp/resources/types.py +30 -24
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +9 -14
- fastmcp/server/auth/auth.py +197 -46
- fastmcp/server/auth/handlers/authorize.py +326 -0
- fastmcp/server/auth/jwt_issuer.py +236 -0
- fastmcp/server/auth/middleware.py +96 -0
- fastmcp/server/auth/oauth_proxy.py +1469 -298
- fastmcp/server/auth/oidc_proxy.py +91 -20
- fastmcp/server/auth/providers/auth0.py +40 -21
- fastmcp/server/auth/providers/aws.py +29 -3
- fastmcp/server/auth/providers/azure.py +312 -131
- fastmcp/server/auth/providers/debug.py +114 -0
- fastmcp/server/auth/providers/descope.py +86 -29
- fastmcp/server/auth/providers/discord.py +308 -0
- fastmcp/server/auth/providers/github.py +29 -8
- fastmcp/server/auth/providers/google.py +48 -9
- fastmcp/server/auth/providers/in_memory.py +29 -5
- fastmcp/server/auth/providers/introspection.py +281 -0
- fastmcp/server/auth/providers/jwt.py +48 -31
- fastmcp/server/auth/providers/oci.py +233 -0
- fastmcp/server/auth/providers/scalekit.py +238 -0
- fastmcp/server/auth/providers/supabase.py +188 -0
- fastmcp/server/auth/providers/workos.py +35 -17
- fastmcp/server/context.py +236 -116
- fastmcp/server/dependencies.py +503 -18
- fastmcp/server/elicitation.py +286 -48
- fastmcp/server/event_store.py +177 -0
- fastmcp/server/http.py +71 -20
- fastmcp/server/low_level.py +165 -2
- fastmcp/server/middleware/__init__.py +1 -1
- fastmcp/server/middleware/caching.py +476 -0
- fastmcp/server/middleware/error_handling.py +14 -10
- fastmcp/server/middleware/logging.py +50 -39
- fastmcp/server/middleware/middleware.py +29 -16
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/middleware/tool_injection.py +116 -0
- fastmcp/server/openapi/__init__.py +35 -0
- fastmcp/{experimental/server → server}/openapi/components.py +15 -10
- fastmcp/{experimental/server → server}/openapi/routing.py +3 -3
- fastmcp/{experimental/server → server}/openapi/server.py +6 -5
- fastmcp/server/proxy.py +72 -48
- fastmcp/server/server.py +1415 -733
- fastmcp/server/tasks/__init__.py +21 -0
- fastmcp/server/tasks/capabilities.py +22 -0
- fastmcp/server/tasks/config.py +89 -0
- fastmcp/server/tasks/converters.py +205 -0
- fastmcp/server/tasks/handlers.py +356 -0
- fastmcp/server/tasks/keys.py +93 -0
- fastmcp/server/tasks/protocol.py +355 -0
- fastmcp/server/tasks/subscriptions.py +205 -0
- fastmcp/settings.py +125 -113
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +138 -55
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +12 -21
- fastmcp/utilities/cli.py +67 -28
- fastmcp/utilities/components.py +10 -5
- fastmcp/utilities/inspect.py +79 -23
- fastmcp/utilities/json_schema.py +4 -4
- fastmcp/utilities/json_schema_type.py +8 -8
- fastmcp/utilities/logging.py +118 -8
- fastmcp/utilities/mcp_config.py +1 -2
- fastmcp/utilities/mcp_server_config/__init__.py +3 -3
- fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +5 -5
- fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
- fastmcp/{experimental/utilities → utilities}/openapi/README.md +7 -35
- fastmcp/utilities/openapi/__init__.py +63 -0
- fastmcp/{experimental/utilities → utilities}/openapi/director.py +14 -15
- fastmcp/{experimental/utilities → utilities}/openapi/formatters.py +5 -5
- fastmcp/{experimental/utilities → utilities}/openapi/json_schema_converter.py +7 -3
- fastmcp/{experimental/utilities → utilities}/openapi/parser.py +37 -16
- fastmcp/utilities/tests.py +92 -5
- fastmcp/utilities/types.py +86 -16
- fastmcp/utilities/ui.py +626 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/METADATA +24 -15
- fastmcp-2.14.0.dist-info/RECORD +156 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/WHEEL +1 -1
- fastmcp/cli/claude.py +0 -135
- fastmcp/server/auth/providers/bearer.py +0 -25
- fastmcp/server/openapi.py +0 -1083
- fastmcp/utilities/openapi.py +0 -1568
- fastmcp/utilities/storage.py +0 -204
- fastmcp-2.12.5.dist-info/RECORD +0 -134
- fastmcp/{experimental/server → server}/openapi/README.md +0 -0
- fastmcp/{experimental/utilities → utilities}/openapi/models.py +3 -3
- fastmcp/{experimental/utilities → utilities}/openapi/schemas.py +2 -2
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/client/elicitation.py
CHANGED
|
@@ -7,7 +7,7 @@ import mcp.types
|
|
|
7
7
|
from mcp import ClientSession
|
|
8
8
|
from mcp.client.session import ElicitationFnT
|
|
9
9
|
from mcp.shared.context import LifespanContextT, RequestContext
|
|
10
|
-
from mcp.types import ElicitRequestParams
|
|
10
|
+
from mcp.types import ElicitRequestFormParams, ElicitRequestParams
|
|
11
11
|
from mcp.types import ElicitResult as MCPElicitResult
|
|
12
12
|
from pydantic_core import to_jsonable_python
|
|
13
13
|
from typing_extensions import TypeVar
|
|
@@ -26,7 +26,8 @@ class ElicitResult(MCPElicitResult, Generic[T]):
|
|
|
26
26
|
ElicitationHandler: TypeAlias = Callable[
|
|
27
27
|
[
|
|
28
28
|
str, # message
|
|
29
|
-
type[T]
|
|
29
|
+
type[T]
|
|
30
|
+
| None, # a class for creating a structured response (None for URL elicitation)
|
|
30
31
|
ElicitRequestParams,
|
|
31
32
|
RequestContext[ClientSession, LifespanContextT],
|
|
32
33
|
],
|
|
@@ -42,10 +43,15 @@ def create_elicitation_callback(
|
|
|
42
43
|
params: ElicitRequestParams,
|
|
43
44
|
) -> MCPElicitResult | mcp.types.ErrorData:
|
|
44
45
|
try:
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
# requestedSchema only exists on ElicitRequestFormParams, not ElicitRequestURLParams
|
|
47
|
+
if isinstance(params, ElicitRequestFormParams):
|
|
48
|
+
if params.requestedSchema == {"type": "object", "properties": {}}:
|
|
49
|
+
response_type = None
|
|
50
|
+
else:
|
|
51
|
+
response_type = json_schema_to_type(params.requestedSchema)
|
|
47
52
|
else:
|
|
48
|
-
|
|
53
|
+
# URL-based elicitation doesn't have a schema
|
|
54
|
+
response_type = None
|
|
49
55
|
|
|
50
56
|
result = await elicitation_handler(
|
|
51
57
|
params.message, response_type, params, context
|
fastmcp/client/logging.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from collections.abc import Awaitable, Callable
|
|
2
|
+
from logging import Logger
|
|
2
3
|
from typing import TypeAlias
|
|
3
4
|
|
|
4
5
|
from mcp.client.session import LoggingFnT
|
|
@@ -6,7 +7,8 @@ from mcp.types import LoggingMessageNotificationParams
|
|
|
6
7
|
|
|
7
8
|
from fastmcp.utilities.logging import get_logger
|
|
8
9
|
|
|
9
|
-
logger = get_logger(__name__)
|
|
10
|
+
logger: Logger = get_logger(name=__name__)
|
|
11
|
+
from_server_logger: Logger = get_logger(name="fastmcp.client.from_server")
|
|
10
12
|
|
|
11
13
|
LogMessage: TypeAlias = LoggingMessageNotificationParams
|
|
12
14
|
LogHandler: TypeAlias = Callable[[LogMessage], Awaitable[None]]
|
|
@@ -14,30 +16,32 @@ LogHandler: TypeAlias = Callable[[LogMessage], Awaitable[None]]
|
|
|
14
16
|
|
|
15
17
|
async def default_log_handler(message: LogMessage) -> None:
|
|
16
18
|
"""Default handler that properly routes server log messages to appropriate log levels."""
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
# data can be any JSON-serializable type, not just a dict
|
|
20
|
+
data = message.data
|
|
19
21
|
|
|
20
22
|
# Map MCP log levels to Python logging levels
|
|
21
23
|
level_map = {
|
|
22
|
-
"debug":
|
|
23
|
-
"info":
|
|
24
|
-
"notice":
|
|
25
|
-
"warning":
|
|
26
|
-
"error":
|
|
27
|
-
"critical":
|
|
28
|
-
"alert":
|
|
29
|
-
"emergency":
|
|
24
|
+
"debug": from_server_logger.debug,
|
|
25
|
+
"info": from_server_logger.info,
|
|
26
|
+
"notice": from_server_logger.info, # Python doesn't have 'notice', map to info
|
|
27
|
+
"warning": from_server_logger.warning,
|
|
28
|
+
"error": from_server_logger.error,
|
|
29
|
+
"critical": from_server_logger.critical,
|
|
30
|
+
"alert": from_server_logger.critical, # Map alert to critical
|
|
31
|
+
"emergency": from_server_logger.critical, # Map emergency to critical
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
# Get the appropriate logging function based on the message level
|
|
33
35
|
log_fn = level_map.get(message.level.lower(), logger.info)
|
|
34
36
|
|
|
35
37
|
# Include logger name if available
|
|
38
|
+
msg_prefix: str = f"Received {message.level.upper()} from server"
|
|
39
|
+
|
|
36
40
|
if message.logger:
|
|
37
|
-
|
|
41
|
+
msg_prefix += f" ({message.logger})"
|
|
38
42
|
|
|
39
|
-
# Log with appropriate level and
|
|
40
|
-
log_fn(f"
|
|
43
|
+
# Log with appropriate level and data
|
|
44
|
+
log_fn(msg=f"{msg_prefix}: {data}")
|
|
41
45
|
|
|
42
46
|
|
|
43
47
|
def create_log_callback(handler: LogHandler | None = None) -> LoggingFnT:
|
fastmcp/client/messages.py
CHANGED
|
@@ -35,16 +35,18 @@ class MessageHandler:
|
|
|
35
35
|
# requests
|
|
36
36
|
case RequestResponder():
|
|
37
37
|
# handle all requests
|
|
38
|
-
|
|
38
|
+
# TODO(ty): remove when ty supports match statement narrowing
|
|
39
|
+
await self.on_request(message) # type: ignore[arg-type]
|
|
39
40
|
|
|
40
41
|
# handle specific requests
|
|
41
|
-
match
|
|
42
|
+
# TODO(ty): remove type ignores when ty supports match statement narrowing
|
|
43
|
+
match message.request.root: # type: ignore[union-attr]
|
|
42
44
|
case mcp.types.PingRequest():
|
|
43
|
-
await self.on_ping(message.request.root)
|
|
45
|
+
await self.on_ping(message.request.root) # type: ignore[union-attr]
|
|
44
46
|
case mcp.types.ListRootsRequest():
|
|
45
|
-
await self.on_list_roots(message.request.root)
|
|
47
|
+
await self.on_list_roots(message.request.root) # type: ignore[union-attr]
|
|
46
48
|
case mcp.types.CreateMessageRequest():
|
|
47
|
-
await self.on_create_message(message.request.root)
|
|
49
|
+
await self.on_create_message(message.request.root) # type: ignore[union-attr]
|
|
48
50
|
|
|
49
51
|
# notifications
|
|
50
52
|
case mcp.types.ServerNotification():
|
fastmcp/client/oauth_callback.py
CHANGED
|
@@ -7,17 +7,26 @@ and display styled responses to users.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
import asyncio
|
|
11
10
|
from dataclasses import dataclass
|
|
12
11
|
|
|
12
|
+
import anyio
|
|
13
13
|
from starlette.applications import Starlette
|
|
14
14
|
from starlette.requests import Request
|
|
15
|
-
from starlette.responses import HTMLResponse
|
|
16
15
|
from starlette.routing import Route
|
|
17
16
|
from uvicorn import Config, Server
|
|
18
17
|
|
|
19
18
|
from fastmcp.utilities.http import find_available_port
|
|
20
19
|
from fastmcp.utilities.logging import get_logger
|
|
20
|
+
from fastmcp.utilities.ui import (
|
|
21
|
+
HELPER_TEXT_STYLES,
|
|
22
|
+
INFO_BOX_STYLES,
|
|
23
|
+
STATUS_MESSAGE_STYLES,
|
|
24
|
+
create_info_box,
|
|
25
|
+
create_logo,
|
|
26
|
+
create_page,
|
|
27
|
+
create_secure_html_response,
|
|
28
|
+
create_status_message,
|
|
29
|
+
)
|
|
21
30
|
|
|
22
31
|
logger = get_logger(__name__)
|
|
23
32
|
|
|
@@ -29,155 +38,43 @@ def create_callback_html(
|
|
|
29
38
|
server_url: str | None = None,
|
|
30
39
|
) -> str:
|
|
31
40
|
"""Create a styled HTML response for OAuth callbacks."""
|
|
32
|
-
logo_url = "https://gofastmcp.com/assets/brand/blue-logo.png"
|
|
33
|
-
|
|
34
41
|
# Build the main status message
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
icon_bg = "#10b98120"
|
|
39
|
-
else:
|
|
40
|
-
status_title = "Authentication failed"
|
|
41
|
-
status_icon = "✕"
|
|
42
|
-
icon_bg = "#ef444420"
|
|
42
|
+
status_title = (
|
|
43
|
+
"Authentication successful" if is_success else "Authentication failed"
|
|
44
|
+
)
|
|
43
45
|
|
|
44
46
|
# Add detail info box for both success and error cases
|
|
45
47
|
detail_info = ""
|
|
46
48
|
if is_success and server_url:
|
|
47
|
-
detail_info =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
</div>
|
|
51
|
-
"""
|
|
49
|
+
detail_info = create_info_box(
|
|
50
|
+
f"Connected to: {server_url}", centered=True, monospace=True
|
|
51
|
+
)
|
|
52
52
|
elif not is_success:
|
|
53
|
-
detail_info =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return f"""
|
|
60
|
-
<!DOCTYPE html>
|
|
61
|
-
<html lang="en">
|
|
62
|
-
<head>
|
|
63
|
-
<meta charset="UTF-8">
|
|
64
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
65
|
-
<title>{title}</title>
|
|
66
|
-
<style>
|
|
67
|
-
* {{
|
|
68
|
-
margin: 0;
|
|
69
|
-
padding: 0;
|
|
70
|
-
box-sizing: border-box;
|
|
71
|
-
}}
|
|
72
|
-
|
|
73
|
-
body {{
|
|
74
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
75
|
-
margin: 0;
|
|
76
|
-
padding: 0;
|
|
77
|
-
min-height: 100vh;
|
|
78
|
-
display: flex;
|
|
79
|
-
align-items: center;
|
|
80
|
-
justify-content: center;
|
|
81
|
-
background: #ffffff;
|
|
82
|
-
color: #0a0a0a;
|
|
83
|
-
}}
|
|
84
|
-
|
|
85
|
-
.container {{
|
|
86
|
-
background: #ffffff;
|
|
87
|
-
border: 1px solid #e5e5e5;
|
|
88
|
-
padding: 3rem 2rem;
|
|
89
|
-
border-radius: 0.75rem;
|
|
90
|
-
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
|
|
91
|
-
text-align: center;
|
|
92
|
-
max-width: 28rem;
|
|
93
|
-
margin: 1rem;
|
|
94
|
-
position: relative;
|
|
95
|
-
}}
|
|
96
|
-
|
|
97
|
-
.logo {{
|
|
98
|
-
width: 60px;
|
|
99
|
-
height: auto;
|
|
100
|
-
margin-bottom: 2rem;
|
|
101
|
-
display: block;
|
|
102
|
-
margin-left: auto;
|
|
103
|
-
margin-right: auto;
|
|
104
|
-
}}
|
|
105
|
-
|
|
106
|
-
.status-message {{
|
|
107
|
-
display: flex;
|
|
108
|
-
align-items: center;
|
|
109
|
-
justify-content: center;
|
|
110
|
-
gap: 0.75rem;
|
|
111
|
-
margin-bottom: 1.5rem;
|
|
112
|
-
}}
|
|
113
|
-
|
|
114
|
-
.status-icon {{
|
|
115
|
-
font-size: 1.5rem;
|
|
116
|
-
line-height: 1;
|
|
117
|
-
display: inline-flex;
|
|
118
|
-
align-items: center;
|
|
119
|
-
justify-content: center;
|
|
120
|
-
width: 2rem;
|
|
121
|
-
height: 2rem;
|
|
122
|
-
background: {icon_bg};
|
|
123
|
-
border-radius: 0.5rem;
|
|
124
|
-
flex-shrink: 0;
|
|
125
|
-
}}
|
|
126
|
-
|
|
127
|
-
.message {{
|
|
128
|
-
font-size: 1.125rem;
|
|
129
|
-
line-height: 1.75;
|
|
130
|
-
color: #0a0a0a;
|
|
131
|
-
font-weight: 600;
|
|
132
|
-
text-align: left;
|
|
133
|
-
}}
|
|
134
|
-
|
|
135
|
-
.info-box {{
|
|
136
|
-
background: #f5f5f5;
|
|
137
|
-
border: 1px solid #e5e5e5;
|
|
138
|
-
border-radius: 0.5rem;
|
|
139
|
-
padding: 0.875rem;
|
|
140
|
-
margin: 1.25rem 0;
|
|
141
|
-
font-size: 0.875rem;
|
|
142
|
-
color: #525252;
|
|
143
|
-
font-family: 'SF Mono', 'Monaco', 'Consolas', 'Courier New', monospace;
|
|
144
|
-
text-align: left;
|
|
145
|
-
}}
|
|
146
|
-
|
|
147
|
-
.info-box.error {{
|
|
148
|
-
background: #fef2f2;
|
|
149
|
-
border-color: #fecaca;
|
|
150
|
-
color: #991b1b;
|
|
151
|
-
}}
|
|
152
|
-
|
|
153
|
-
.info-box strong {{
|
|
154
|
-
color: #0a0a0a;
|
|
155
|
-
font-weight: 600;
|
|
156
|
-
}}
|
|
157
|
-
|
|
158
|
-
.close-instruction {{
|
|
159
|
-
font-size: 0.875rem;
|
|
160
|
-
color: #737373;
|
|
161
|
-
margin-top: 1.5rem;
|
|
162
|
-
}}
|
|
163
|
-
</style>
|
|
164
|
-
</head>
|
|
165
|
-
<body>
|
|
53
|
+
detail_info = create_info_box(
|
|
54
|
+
message, is_error=True, centered=True, monospace=True
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Build the page content
|
|
58
|
+
content = f"""
|
|
166
59
|
<div class="container">
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
<span class="status-icon">{status_icon}</span>
|
|
170
|
-
<div class="message">{status_title}</div>
|
|
171
|
-
</div>
|
|
60
|
+
{create_logo()}
|
|
61
|
+
{create_status_message(status_title, is_success=is_success)}
|
|
172
62
|
{detail_info}
|
|
173
63
|
<div class="close-instruction">
|
|
174
64
|
You can safely close this tab now.
|
|
175
65
|
</div>
|
|
176
66
|
</div>
|
|
177
|
-
</body>
|
|
178
|
-
</html>
|
|
179
67
|
"""
|
|
180
68
|
|
|
69
|
+
# Additional styles needed for this page
|
|
70
|
+
additional_styles = STATUS_MESSAGE_STYLES + INFO_BOX_STYLES + HELPER_TEXT_STYLES
|
|
71
|
+
|
|
72
|
+
return create_page(
|
|
73
|
+
content=content,
|
|
74
|
+
title=title,
|
|
75
|
+
additional_styles=additional_styles,
|
|
76
|
+
)
|
|
77
|
+
|
|
181
78
|
|
|
182
79
|
@dataclass
|
|
183
80
|
class CallbackResponse:
|
|
@@ -194,11 +91,21 @@ class CallbackResponse:
|
|
|
194
91
|
return {k: v for k, v in self.__dict__.items() if v is not None}
|
|
195
92
|
|
|
196
93
|
|
|
94
|
+
@dataclass
|
|
95
|
+
class OAuthCallbackResult:
|
|
96
|
+
"""Container for OAuth callback results, used with anyio.Event for async coordination."""
|
|
97
|
+
|
|
98
|
+
code: str | None = None
|
|
99
|
+
state: str | None = None
|
|
100
|
+
error: Exception | None = None
|
|
101
|
+
|
|
102
|
+
|
|
197
103
|
def create_oauth_callback_server(
|
|
198
104
|
port: int,
|
|
199
105
|
callback_path: str = "/callback",
|
|
200
106
|
server_url: str | None = None,
|
|
201
|
-
|
|
107
|
+
result_container: OAuthCallbackResult | None = None,
|
|
108
|
+
result_ready: anyio.Event | None = None,
|
|
202
109
|
) -> Server:
|
|
203
110
|
"""
|
|
204
111
|
Create an OAuth callback server.
|
|
@@ -207,7 +114,8 @@ def create_oauth_callback_server(
|
|
|
207
114
|
port: The port to run the server on
|
|
208
115
|
callback_path: The path to listen for OAuth redirects on
|
|
209
116
|
server_url: Optional server URL to display in success messages
|
|
210
|
-
|
|
117
|
+
result_container: Optional container to store callback results
|
|
118
|
+
result_ready: Optional event to signal when callback is received
|
|
211
119
|
|
|
212
120
|
Returns:
|
|
213
121
|
Configured uvicorn Server instance (not yet running)
|
|
@@ -221,32 +129,36 @@ def create_oauth_callback_server(
|
|
|
221
129
|
if callback_response.error:
|
|
222
130
|
error_desc = callback_response.error_description or "Unknown error"
|
|
223
131
|
|
|
224
|
-
#
|
|
225
|
-
if
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
132
|
+
# Create user-friendly error messages
|
|
133
|
+
if callback_response.error == "access_denied":
|
|
134
|
+
user_message = "Access was denied by the authorization server."
|
|
135
|
+
else:
|
|
136
|
+
user_message = f"Authorization failed: {error_desc}"
|
|
137
|
+
|
|
138
|
+
# Store error and signal completion if result tracking provided
|
|
139
|
+
if result_container is not None and result_ready is not None:
|
|
140
|
+
result_container.error = RuntimeError(user_message)
|
|
141
|
+
result_ready.set()
|
|
231
142
|
|
|
232
|
-
return
|
|
143
|
+
return create_secure_html_response(
|
|
233
144
|
create_callback_html(
|
|
234
|
-
|
|
145
|
+
user_message,
|
|
235
146
|
is_success=False,
|
|
236
147
|
),
|
|
237
148
|
status_code=400,
|
|
238
149
|
)
|
|
239
150
|
|
|
240
151
|
if not callback_response.code:
|
|
241
|
-
|
|
242
|
-
if response_future and not response_future.done():
|
|
243
|
-
response_future.set_exception(
|
|
244
|
-
RuntimeError("OAuth callback missing authorization code")
|
|
245
|
-
)
|
|
152
|
+
user_message = "No authorization code was received from the server."
|
|
246
153
|
|
|
247
|
-
|
|
154
|
+
# Store error and signal completion if result tracking provided
|
|
155
|
+
if result_container is not None and result_ready is not None:
|
|
156
|
+
result_container.error = RuntimeError(user_message)
|
|
157
|
+
result_ready.set()
|
|
158
|
+
|
|
159
|
+
return create_secure_html_response(
|
|
248
160
|
create_callback_html(
|
|
249
|
-
|
|
161
|
+
user_message,
|
|
250
162
|
is_success=False,
|
|
251
163
|
),
|
|
252
164
|
status_code=400,
|
|
@@ -254,29 +166,30 @@ def create_oauth_callback_server(
|
|
|
254
166
|
|
|
255
167
|
# Check for missing state parameter (indicates OAuth flow issue)
|
|
256
168
|
if callback_response.state is None:
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
)
|
|
264
|
-
|
|
265
|
-
|
|
169
|
+
user_message = (
|
|
170
|
+
"The OAuth server did not return the expected state parameter."
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Store error and signal completion if result tracking provided
|
|
174
|
+
if result_container is not None and result_ready is not None:
|
|
175
|
+
result_container.error = RuntimeError(user_message)
|
|
176
|
+
result_ready.set()
|
|
177
|
+
|
|
178
|
+
return create_secure_html_response(
|
|
266
179
|
create_callback_html(
|
|
267
|
-
|
|
180
|
+
user_message,
|
|
268
181
|
is_success=False,
|
|
269
182
|
),
|
|
270
183
|
status_code=400,
|
|
271
184
|
)
|
|
272
185
|
|
|
273
|
-
# Success case
|
|
274
|
-
if
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
)
|
|
186
|
+
# Success case - store result and signal completion if result tracking provided
|
|
187
|
+
if result_container is not None and result_ready is not None:
|
|
188
|
+
result_container.code = callback_response.code
|
|
189
|
+
result_container.state = callback_response.state
|
|
190
|
+
result_ready.set()
|
|
278
191
|
|
|
279
|
-
return
|
|
192
|
+
return create_secure_html_response(
|
|
280
193
|
create_callback_html("", is_success=True, server_url=server_url)
|
|
281
194
|
)
|
|
282
195
|
|
|
@@ -289,6 +202,7 @@ def create_oauth_callback_server(
|
|
|
289
202
|
port=port,
|
|
290
203
|
lifespan="off",
|
|
291
204
|
log_level="warning",
|
|
205
|
+
ws="websockets-sansio",
|
|
292
206
|
)
|
|
293
207
|
)
|
|
294
208
|
|
fastmcp/client/roots.py
CHANGED
|
@@ -34,7 +34,8 @@ def create_roots_callback(
|
|
|
34
34
|
handler: RootsList | RootsHandler,
|
|
35
35
|
) -> ListRootsFnT:
|
|
36
36
|
if isinstance(handler, list):
|
|
37
|
-
|
|
37
|
+
# TODO(ty): remove when ty supports isinstance union narrowing
|
|
38
|
+
return _create_roots_callback_from_roots(handler) # type: ignore[arg-type]
|
|
38
39
|
elif inspect.isfunction(handler):
|
|
39
40
|
return _create_roots_callback_from_fn(handler)
|
|
40
41
|
else:
|
fastmcp/client/sampling.py
CHANGED
|
@@ -11,7 +11,7 @@ from mcp.types import SamplingMessage
|
|
|
11
11
|
|
|
12
12
|
from fastmcp.server.sampling.handler import ServerSamplingHandler
|
|
13
13
|
|
|
14
|
-
__all__ = ["
|
|
14
|
+
__all__ = ["SamplingHandler", "SamplingMessage", "SamplingParams"]
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
ClientSamplingHandler: TypeAlias = Callable[
|