fastmcp 2.12.5__py3-none-any.whl → 2.13.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/cli/cli.py +7 -6
- fastmcp/cli/install/claude_code.py +6 -6
- fastmcp/cli/install/claude_desktop.py +3 -3
- fastmcp/cli/install/cursor.py +7 -7
- fastmcp/cli/install/gemini_cli.py +3 -3
- fastmcp/cli/install/mcp_json.py +3 -3
- fastmcp/cli/run.py +13 -8
- fastmcp/client/auth/oauth.py +100 -208
- fastmcp/client/client.py +11 -11
- fastmcp/client/logging.py +18 -14
- fastmcp/client/oauth_callback.py +85 -171
- fastmcp/client/transports.py +77 -22
- fastmcp/contrib/component_manager/component_service.py +6 -6
- fastmcp/contrib/mcp_mixin/README.md +32 -1
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +4 -0
- fastmcp/experimental/utilities/openapi/parser.py +23 -3
- fastmcp/prompts/prompt.py +13 -6
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/resource.py +13 -6
- fastmcp/resources/resource_manager.py +5 -164
- fastmcp/resources/template.py +107 -17
- fastmcp/resources/types.py +30 -24
- fastmcp/server/auth/auth.py +40 -32
- fastmcp/server/auth/handlers/authorize.py +324 -0
- fastmcp/server/auth/jwt_issuer.py +236 -0
- fastmcp/server/auth/middleware.py +96 -0
- fastmcp/server/auth/oauth_proxy.py +1256 -242
- fastmcp/server/auth/oidc_proxy.py +23 -6
- fastmcp/server/auth/providers/auth0.py +40 -21
- fastmcp/server/auth/providers/aws.py +29 -3
- fastmcp/server/auth/providers/azure.py +178 -127
- fastmcp/server/auth/providers/descope.py +4 -6
- fastmcp/server/auth/providers/github.py +29 -8
- fastmcp/server/auth/providers/google.py +30 -9
- fastmcp/server/auth/providers/introspection.py +281 -0
- fastmcp/server/auth/providers/jwt.py +8 -2
- fastmcp/server/auth/providers/scalekit.py +179 -0
- fastmcp/server/auth/providers/supabase.py +172 -0
- fastmcp/server/auth/providers/workos.py +32 -14
- fastmcp/server/context.py +122 -36
- fastmcp/server/http.py +58 -18
- fastmcp/server/low_level.py +121 -2
- fastmcp/server/middleware/caching.py +469 -0
- fastmcp/server/middleware/error_handling.py +6 -2
- fastmcp/server/middleware/logging.py +48 -37
- fastmcp/server/middleware/middleware.py +28 -15
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/middleware/tool_injection.py +116 -0
- fastmcp/server/proxy.py +6 -6
- fastmcp/server/server.py +683 -207
- fastmcp/settings.py +24 -10
- fastmcp/tools/tool.py +7 -3
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +3 -3
- fastmcp/utilities/cli.py +62 -22
- fastmcp/utilities/components.py +5 -0
- fastmcp/utilities/inspect.py +77 -21
- fastmcp/utilities/logging.py +118 -8
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
- fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
- fastmcp/utilities/tests.py +87 -4
- fastmcp/utilities/types.py +1 -1
- fastmcp/utilities/ui.py +617 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0.dist-info}/METADATA +10 -6
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0.dist-info}/RECORD +70 -63
- fastmcp/cli/claude.py +0 -135
- fastmcp/utilities/storage.py +0 -204
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
from collections.abc import Awaitable
|
|
4
|
+
from collections.abc import Awaitable, Sequence
|
|
5
5
|
from dataclasses import dataclass, field, replace
|
|
6
6
|
from datetime import datetime, timezone
|
|
7
7
|
from functools import partial
|
|
@@ -99,6 +99,8 @@ class Middleware:
|
|
|
99
99
|
handler = call_next
|
|
100
100
|
|
|
101
101
|
match context.method:
|
|
102
|
+
case "initialize":
|
|
103
|
+
handler = partial(self.on_initialize, call_next=handler)
|
|
102
104
|
case "tools/call":
|
|
103
105
|
handler = partial(self.on_call_tool, call_next=handler)
|
|
104
106
|
case "resources/read":
|
|
@@ -133,18 +135,25 @@ class Middleware:
|
|
|
133
135
|
|
|
134
136
|
async def on_request(
|
|
135
137
|
self,
|
|
136
|
-
context: MiddlewareContext[mt.Request],
|
|
137
|
-
call_next: CallNext[mt.Request, Any],
|
|
138
|
+
context: MiddlewareContext[mt.Request[Any, Any]],
|
|
139
|
+
call_next: CallNext[mt.Request[Any, Any], Any],
|
|
138
140
|
) -> Any:
|
|
139
141
|
return await call_next(context)
|
|
140
142
|
|
|
141
143
|
async def on_notification(
|
|
142
144
|
self,
|
|
143
|
-
context: MiddlewareContext[mt.Notification],
|
|
144
|
-
call_next: CallNext[mt.Notification, Any],
|
|
145
|
+
context: MiddlewareContext[mt.Notification[Any, Any]],
|
|
146
|
+
call_next: CallNext[mt.Notification[Any, Any], Any],
|
|
145
147
|
) -> Any:
|
|
146
148
|
return await call_next(context)
|
|
147
149
|
|
|
150
|
+
async def on_initialize(
|
|
151
|
+
self,
|
|
152
|
+
context: MiddlewareContext[mt.InitializeRequestParams],
|
|
153
|
+
call_next: CallNext[mt.InitializeRequestParams, None],
|
|
154
|
+
) -> None:
|
|
155
|
+
return await call_next(context)
|
|
156
|
+
|
|
148
157
|
async def on_call_tool(
|
|
149
158
|
self,
|
|
150
159
|
context: MiddlewareContext[mt.CallToolRequestParams],
|
|
@@ -155,8 +164,10 @@ class Middleware:
|
|
|
155
164
|
async def on_read_resource(
|
|
156
165
|
self,
|
|
157
166
|
context: MiddlewareContext[mt.ReadResourceRequestParams],
|
|
158
|
-
call_next: CallNext[
|
|
159
|
-
|
|
167
|
+
call_next: CallNext[
|
|
168
|
+
mt.ReadResourceRequestParams, Sequence[ReadResourceContents]
|
|
169
|
+
],
|
|
170
|
+
) -> Sequence[ReadResourceContents]:
|
|
160
171
|
return await call_next(context)
|
|
161
172
|
|
|
162
173
|
async def on_get_prompt(
|
|
@@ -169,27 +180,29 @@ class Middleware:
|
|
|
169
180
|
async def on_list_tools(
|
|
170
181
|
self,
|
|
171
182
|
context: MiddlewareContext[mt.ListToolsRequest],
|
|
172
|
-
call_next: CallNext[mt.ListToolsRequest,
|
|
173
|
-
) ->
|
|
183
|
+
call_next: CallNext[mt.ListToolsRequest, Sequence[Tool]],
|
|
184
|
+
) -> Sequence[Tool]:
|
|
174
185
|
return await call_next(context)
|
|
175
186
|
|
|
176
187
|
async def on_list_resources(
|
|
177
188
|
self,
|
|
178
189
|
context: MiddlewareContext[mt.ListResourcesRequest],
|
|
179
|
-
call_next: CallNext[mt.ListResourcesRequest,
|
|
180
|
-
) ->
|
|
190
|
+
call_next: CallNext[mt.ListResourcesRequest, Sequence[Resource]],
|
|
191
|
+
) -> Sequence[Resource]:
|
|
181
192
|
return await call_next(context)
|
|
182
193
|
|
|
183
194
|
async def on_list_resource_templates(
|
|
184
195
|
self,
|
|
185
196
|
context: MiddlewareContext[mt.ListResourceTemplatesRequest],
|
|
186
|
-
call_next: CallNext[
|
|
187
|
-
|
|
197
|
+
call_next: CallNext[
|
|
198
|
+
mt.ListResourceTemplatesRequest, Sequence[ResourceTemplate]
|
|
199
|
+
],
|
|
200
|
+
) -> Sequence[ResourceTemplate]:
|
|
188
201
|
return await call_next(context)
|
|
189
202
|
|
|
190
203
|
async def on_list_prompts(
|
|
191
204
|
self,
|
|
192
205
|
context: MiddlewareContext[mt.ListPromptsRequest],
|
|
193
|
-
call_next: CallNext[mt.ListPromptsRequest,
|
|
194
|
-
) ->
|
|
206
|
+
call_next: CallNext[mt.ListPromptsRequest, Sequence[Prompt]],
|
|
207
|
+
) -> Sequence[Prompt]:
|
|
195
208
|
return await call_next(context)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"""Rate limiting middleware for protecting FastMCP servers from abuse."""
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
3
|
import time
|
|
5
4
|
from collections import defaultdict, deque
|
|
6
5
|
from collections.abc import Callable
|
|
7
6
|
from typing import Any
|
|
8
7
|
|
|
8
|
+
import anyio
|
|
9
9
|
from mcp import McpError
|
|
10
10
|
from mcp.types import ErrorData
|
|
11
11
|
|
|
@@ -33,7 +33,7 @@ class TokenBucketRateLimiter:
|
|
|
33
33
|
self.refill_rate = refill_rate
|
|
34
34
|
self.tokens = capacity
|
|
35
35
|
self.last_refill = time.time()
|
|
36
|
-
self._lock =
|
|
36
|
+
self._lock = anyio.Lock()
|
|
37
37
|
|
|
38
38
|
async def consume(self, tokens: int = 1) -> bool:
|
|
39
39
|
"""Try to consume tokens from the bucket.
|
|
@@ -71,7 +71,7 @@ class SlidingWindowRateLimiter:
|
|
|
71
71
|
self.max_requests = max_requests
|
|
72
72
|
self.window_seconds = window_seconds
|
|
73
73
|
self.requests = deque()
|
|
74
|
-
self._lock =
|
|
74
|
+
self._lock = anyio.Lock()
|
|
75
75
|
|
|
76
76
|
async def is_allowed(self) -> bool:
|
|
77
77
|
"""Check if a request is allowed."""
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""A middleware for injecting tools into the MCP server context."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from logging import Logger
|
|
5
|
+
from typing import Annotated, Any
|
|
6
|
+
|
|
7
|
+
import mcp.types
|
|
8
|
+
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
|
9
|
+
from mcp.types import Prompt
|
|
10
|
+
from pydantic import AnyUrl
|
|
11
|
+
from typing_extensions import override
|
|
12
|
+
|
|
13
|
+
from fastmcp.server.context import Context
|
|
14
|
+
from fastmcp.server.middleware.middleware import CallNext, Middleware, MiddlewareContext
|
|
15
|
+
from fastmcp.tools.tool import Tool, ToolResult
|
|
16
|
+
from fastmcp.utilities.logging import get_logger
|
|
17
|
+
|
|
18
|
+
logger: Logger = get_logger(name=__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ToolInjectionMiddleware(Middleware):
|
|
22
|
+
"""A middleware for injecting tools into the context."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, tools: Sequence[Tool]):
|
|
25
|
+
"""Initialize the tool injection middleware."""
|
|
26
|
+
self._tools_to_inject: Sequence[Tool] = tools
|
|
27
|
+
self._tools_to_inject_by_name: dict[str, Tool] = {
|
|
28
|
+
tool.name: tool for tool in tools
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@override
|
|
32
|
+
async def on_list_tools(
|
|
33
|
+
self,
|
|
34
|
+
context: MiddlewareContext[mcp.types.ListToolsRequest],
|
|
35
|
+
call_next: CallNext[mcp.types.ListToolsRequest, Sequence[Tool]],
|
|
36
|
+
) -> Sequence[Tool]:
|
|
37
|
+
"""Inject tools into the response."""
|
|
38
|
+
return [*self._tools_to_inject, *await call_next(context)]
|
|
39
|
+
|
|
40
|
+
@override
|
|
41
|
+
async def on_call_tool(
|
|
42
|
+
self,
|
|
43
|
+
context: MiddlewareContext[mcp.types.CallToolRequestParams],
|
|
44
|
+
call_next: CallNext[mcp.types.CallToolRequestParams, ToolResult],
|
|
45
|
+
) -> ToolResult:
|
|
46
|
+
"""Intercept tool calls to injected tools."""
|
|
47
|
+
if context.message.name in self._tools_to_inject_by_name:
|
|
48
|
+
tool = self._tools_to_inject_by_name[context.message.name]
|
|
49
|
+
return await tool.run(arguments=context.message.arguments or {})
|
|
50
|
+
|
|
51
|
+
return await call_next(context)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def list_prompts(context: Context) -> list[Prompt]:
|
|
55
|
+
"""List prompts available on the server."""
|
|
56
|
+
return await context.list_prompts()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
list_prompts_tool = Tool.from_function(
|
|
60
|
+
fn=list_prompts,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def get_prompt(
|
|
65
|
+
context: Context,
|
|
66
|
+
name: Annotated[str, "The name of the prompt to render."],
|
|
67
|
+
arguments: Annotated[
|
|
68
|
+
dict[str, Any] | None, "The arguments to pass to the prompt."
|
|
69
|
+
] = None,
|
|
70
|
+
) -> mcp.types.GetPromptResult:
|
|
71
|
+
"""Render a prompt available on the server."""
|
|
72
|
+
return await context.get_prompt(name=name, arguments=arguments)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
get_prompt_tool = Tool.from_function(
|
|
76
|
+
fn=get_prompt,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class PromptToolMiddleware(ToolInjectionMiddleware):
|
|
81
|
+
"""A middleware for injecting prompts as tools into the context."""
|
|
82
|
+
|
|
83
|
+
def __init__(self) -> None:
|
|
84
|
+
tools: list[Tool] = [list_prompts_tool, get_prompt_tool]
|
|
85
|
+
super().__init__(tools=tools)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def list_resources(context: Context) -> list[mcp.types.Resource]:
|
|
89
|
+
"""List resources available on the server."""
|
|
90
|
+
return await context.list_resources()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
list_resources_tool = Tool.from_function(
|
|
94
|
+
fn=list_resources,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async def read_resource(
|
|
99
|
+
context: Context,
|
|
100
|
+
uri: Annotated[AnyUrl | str, "The URI of the resource to read."],
|
|
101
|
+
) -> list[ReadResourceContents]:
|
|
102
|
+
"""Read a resource available on the server."""
|
|
103
|
+
return await context.read_resource(uri=uri)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
read_resource_tool = Tool.from_function(
|
|
107
|
+
fn=read_resource,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class ResourceToolMiddleware(ToolInjectionMiddleware):
|
|
112
|
+
"""A middleware for injecting resources as tools into the context."""
|
|
113
|
+
|
|
114
|
+
def __init__(self) -> None:
|
|
115
|
+
tools: list[Tool] = [list_resources_tool, read_resource_tool]
|
|
116
|
+
super().__init__(tools=tools)
|
fastmcp/server/proxy.py
CHANGED
|
@@ -69,7 +69,7 @@ class ProxyManagerMixin:
|
|
|
69
69
|
class ProxyToolManager(ToolManager, ProxyManagerMixin):
|
|
70
70
|
"""A ToolManager that sources its tools from a remote client in addition to local and mounted tools."""
|
|
71
71
|
|
|
72
|
-
def __init__(self, client_factory: ClientFactoryT, **kwargs):
|
|
72
|
+
def __init__(self, client_factory: ClientFactoryT, **kwargs: Any):
|
|
73
73
|
super().__init__(**kwargs)
|
|
74
74
|
self.client_factory = client_factory
|
|
75
75
|
|
|
@@ -123,7 +123,7 @@ class ProxyToolManager(ToolManager, ProxyManagerMixin):
|
|
|
123
123
|
class ProxyResourceManager(ResourceManager, ProxyManagerMixin):
|
|
124
124
|
"""A ResourceManager that sources its resources from a remote client in addition to local and mounted resources."""
|
|
125
125
|
|
|
126
|
-
def __init__(self, client_factory: ClientFactoryT, **kwargs):
|
|
126
|
+
def __init__(self, client_factory: ClientFactoryT, **kwargs: Any):
|
|
127
127
|
super().__init__(**kwargs)
|
|
128
128
|
self.client_factory = client_factory
|
|
129
129
|
|
|
@@ -204,7 +204,7 @@ class ProxyResourceManager(ResourceManager, ProxyManagerMixin):
|
|
|
204
204
|
class ProxyPromptManager(PromptManager, ProxyManagerMixin):
|
|
205
205
|
"""A PromptManager that sources its prompts from a remote client in addition to local and mounted prompts."""
|
|
206
206
|
|
|
207
|
-
def __init__(self, client_factory: ClientFactoryT, **kwargs):
|
|
207
|
+
def __init__(self, client_factory: ClientFactoryT, **kwargs: Any):
|
|
208
208
|
super().__init__(**kwargs)
|
|
209
209
|
self.client_factory = client_factory
|
|
210
210
|
|
|
@@ -258,7 +258,7 @@ class ProxyTool(Tool, MirroredComponent):
|
|
|
258
258
|
A Tool that represents and executes a tool on a remote server.
|
|
259
259
|
"""
|
|
260
260
|
|
|
261
|
-
def __init__(self, client: Client, **kwargs):
|
|
261
|
+
def __init__(self, client: Client, **kwargs: Any):
|
|
262
262
|
super().__init__(**kwargs)
|
|
263
263
|
self._client = client
|
|
264
264
|
|
|
@@ -354,7 +354,7 @@ class ProxyTemplate(ResourceTemplate, MirroredComponent):
|
|
|
354
354
|
A ResourceTemplate that represents and creates resources from a remote server template.
|
|
355
355
|
"""
|
|
356
356
|
|
|
357
|
-
def __init__(self, client: Client, **kwargs):
|
|
357
|
+
def __init__(self, client: Client, **kwargs: Any):
|
|
358
358
|
super().__init__(**kwargs)
|
|
359
359
|
self._client = client
|
|
360
360
|
|
|
@@ -640,7 +640,7 @@ class StatefulProxyClient(ProxyClient[ClientTransportT]):
|
|
|
640
640
|
Note that it is essential to ensure that the proxy server itself is also stateful.
|
|
641
641
|
"""
|
|
642
642
|
|
|
643
|
-
def __init__(self, *args, **kwargs):
|
|
643
|
+
def __init__(self, *args: Any, **kwargs: Any):
|
|
644
644
|
super().__init__(*args, **kwargs)
|
|
645
645
|
self._caches: dict[ServerSession, Client[ClientTransportT]] = {}
|
|
646
646
|
|