fastmcp 2.13.0rc2__py3-none-any.whl → 2.13.0rc3__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.
@@ -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/server.py CHANGED
@@ -7,7 +7,14 @@ import json
7
7
  import re
8
8
  import secrets
9
9
  import warnings
10
- from collections.abc import AsyncIterator, Awaitable, Callable
10
+ from collections.abc import (
11
+ AsyncIterator,
12
+ Awaitable,
13
+ Callable,
14
+ Collection,
15
+ Mapping,
16
+ Sequence,
17
+ )
11
18
  from contextlib import AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager
12
19
  from dataclasses import dataclass
13
20
  from functools import partial
@@ -43,9 +50,11 @@ import fastmcp
43
50
  import fastmcp.server
44
51
  from fastmcp.exceptions import DisabledError, NotFoundError
45
52
  from fastmcp.mcp_config import MCPConfig
46
- from fastmcp.prompts import Prompt, PromptManager
53
+ from fastmcp.prompts import Prompt
47
54
  from fastmcp.prompts.prompt import FunctionPrompt
48
- from fastmcp.resources import Resource, ResourceManager
55
+ from fastmcp.prompts.prompt_manager import PromptManager
56
+ from fastmcp.resources.resource import Resource
57
+ from fastmcp.resources.resource_manager import ResourceManager
49
58
  from fastmcp.resources.template import ResourceTemplate
50
59
  from fastmcp.server.auth import AuthProvider
51
60
  from fastmcp.server.http import (
@@ -56,8 +65,8 @@ from fastmcp.server.http import (
56
65
  from fastmcp.server.low_level import LowLevelServer
57
66
  from fastmcp.server.middleware import Middleware, MiddlewareContext
58
67
  from fastmcp.settings import Settings
59
- from fastmcp.tools import ToolManager
60
68
  from fastmcp.tools.tool import FunctionTool, Tool, ToolResult
69
+ from fastmcp.tools.tool_manager import ToolManager
61
70
  from fastmcp.tools.tool_transform import ToolTransformConfig
62
71
  from fastmcp.utilities.cli import log_server_banner
63
72
  from fastmcp.utilities.components import FastMCPComponent
@@ -66,7 +75,6 @@ from fastmcp.utilities.types import NotSet, NotSetT
66
75
 
67
76
  if TYPE_CHECKING:
68
77
  from fastmcp.client import Client
69
- from fastmcp.client.sampling import ServerSamplingHandler
70
78
  from fastmcp.client.transports import ClientTransport, ClientTransportT
71
79
  from fastmcp.experimental.server.openapi import FastMCPOpenAPI as FastMCPOpenAPINew
72
80
  from fastmcp.experimental.server.openapi.routing import (
@@ -80,6 +88,8 @@ if TYPE_CHECKING:
80
88
  from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap
81
89
  from fastmcp.server.openapi import RouteMapFn as OpenAPIRouteMapFn
82
90
  from fastmcp.server.proxy import FastMCPProxy
91
+ from fastmcp.server.sampling.handler import ServerSamplingHandler
92
+ from fastmcp.tools.tool import ToolResultSerializerType
83
93
 
84
94
  logger = get_logger(__name__)
85
95
 
@@ -141,16 +151,16 @@ class FastMCP(Generic[LifespanResultT]):
141
151
  website_url: str | None = None,
142
152
  icons: list[mcp.types.Icon] | None = None,
143
153
  auth: AuthProvider | None | NotSetT = NotSet,
144
- middleware: list[Middleware] | None = None,
154
+ middleware: Sequence[Middleware] | None = None,
145
155
  lifespan: LifespanCallable | None = None,
146
156
  dependencies: list[str] | None = None,
147
157
  resource_prefix_format: Literal["protocol", "path"] | None = None,
148
158
  mask_error_details: bool | None = None,
149
- tools: list[Tool | Callable[..., Any]] | None = None,
150
- tool_transformations: dict[str, ToolTransformConfig] | None = None,
151
- tool_serializer: Callable[[Any], str] | None = None,
152
- include_tags: set[str] | None = None,
153
- exclude_tags: set[str] | None = None,
159
+ tools: Sequence[Tool | Callable[..., Any]] | None = None,
160
+ tool_transformations: Mapping[str, ToolTransformConfig] | None = None,
161
+ tool_serializer: ToolResultSerializerType | None = None,
162
+ include_tags: Collection[str] | None = None,
163
+ exclude_tags: Collection[str] | None = None,
154
164
  include_fastmcp_meta: bool | None = None,
155
165
  on_duplicate_tools: DuplicateBehavior | None = None,
156
166
  on_duplicate_resources: DuplicateBehavior | None = None,
@@ -179,27 +189,29 @@ class FastMCP(Generic[LifespanResultT]):
179
189
 
180
190
  self._additional_http_routes: list[BaseRoute] = []
181
191
  self._mounted_servers: list[MountedServer] = []
182
- self._tool_manager = ToolManager(
192
+ self._tool_manager: ToolManager = ToolManager(
183
193
  duplicate_behavior=on_duplicate_tools,
184
194
  mask_error_details=mask_error_details,
185
195
  transformations=tool_transformations,
186
196
  )
187
- self._resource_manager = ResourceManager(
197
+ self._resource_manager: ResourceManager = ResourceManager(
188
198
  duplicate_behavior=on_duplicate_resources,
189
199
  mask_error_details=mask_error_details,
190
200
  )
191
- self._prompt_manager = PromptManager(
201
+ self._prompt_manager: PromptManager = PromptManager(
192
202
  duplicate_behavior=on_duplicate_prompts,
193
203
  mask_error_details=mask_error_details,
194
204
  )
195
- self._tool_serializer = tool_serializer
205
+ self._tool_serializer: Callable[[Any], str] | None = tool_serializer
196
206
 
197
207
  self._lifespan: LifespanCallable[LifespanResultT] = lifespan or default_lifespan
198
208
  self._lifespan_result: LifespanResultT | None = None
199
- self._lifespan_result_set = False
209
+ self._lifespan_result_set: bool = False
200
210
 
201
211
  # Generate random ID if no name provided
202
- self._mcp_server = LowLevelServer[LifespanResultT](
212
+ self._mcp_server: LowLevelServer[LifespanResultT, Any] = LowLevelServer[
213
+ LifespanResultT
214
+ ](
203
215
  fastmcp=self,
204
216
  name=name or self.generate_name(),
205
217
  version=version or fastmcp.__version__,
@@ -216,7 +228,7 @@ class FastMCP(Generic[LifespanResultT]):
216
228
  auth = fastmcp.settings.server_auth_class()
217
229
  else:
218
230
  auth = None
219
- self.auth = cast(AuthProvider | None, auth)
231
+ self.auth: AuthProvider | None = cast(AuthProvider | None, auth)
220
232
 
221
233
  if tools:
222
234
  for tool in tools:
@@ -224,15 +236,20 @@ class FastMCP(Generic[LifespanResultT]):
224
236
  tool = Tool.from_function(tool, serializer=self._tool_serializer)
225
237
  self.add_tool(tool)
226
238
 
227
- self.include_tags = include_tags
228
- self.exclude_tags = exclude_tags
229
- self.strict_input_validation = (
239
+ self.include_tags: set[str] | None = (
240
+ set(include_tags) if include_tags is not None else None
241
+ )
242
+ self.exclude_tags: set[str] | None = (
243
+ set(exclude_tags) if exclude_tags is not None else None
244
+ )
245
+
246
+ self.strict_input_validation: bool = (
230
247
  strict_input_validation
231
248
  if strict_input_validation is not None
232
249
  else fastmcp.settings.strict_input_validation
233
250
  )
234
251
 
235
- self.middleware = middleware or []
252
+ self.middleware: list[Middleware] = list(middleware or [])
236
253
 
237
254
  # Set up MCP protocol handlers
238
255
  self._setup_handlers()
@@ -251,14 +268,18 @@ class FastMCP(Generic[LifespanResultT]):
251
268
  DeprecationWarning,
252
269
  stacklevel=2,
253
270
  )
254
- self.dependencies = (
271
+ self.dependencies: list[str] = (
255
272
  dependencies or fastmcp.settings.server_dependencies
256
273
  ) # TODO: Remove (deprecated in v2.11.4)
257
274
 
258
- self.sampling_handler = sampling_handler
259
- self.sampling_handler_behavior = sampling_handler_behavior or "fallback"
275
+ self.sampling_handler: ServerSamplingHandler[LifespanResultT] | None = (
276
+ sampling_handler
277
+ )
278
+ self.sampling_handler_behavior: Literal["always", "fallback"] = (
279
+ sampling_handler_behavior or "fallback"
280
+ )
260
281
 
261
- self.include_fastmcp_meta = (
282
+ self.include_fastmcp_meta: bool = (
262
283
  include_fastmcp_meta
263
284
  if include_fastmcp_meta is not None
264
285
  else fastmcp.settings.include_fastmcp_meta
fastmcp/settings.py CHANGED
@@ -6,6 +6,7 @@ import warnings
6
6
  from pathlib import Path
7
7
  from typing import TYPE_CHECKING, Annotated, Any, Literal
8
8
 
9
+ from platformdirs import user_data_dir
9
10
  from pydantic import Field, ImportString, field_validator
10
11
  from pydantic.fields import FieldInfo
11
12
  from pydantic_settings import (
@@ -150,7 +151,7 @@ class Settings(BaseSettings):
150
151
  )
151
152
  return self
152
153
 
153
- home: Path = Path.home() / ".fastmcp"
154
+ home: Path = Path(user_data_dir("fastmcp", appauthor=False))
154
155
 
155
156
  test_mode: bool = False
156
157
 
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import warnings
4
- from collections.abc import Callable
4
+ from collections.abc import Callable, Mapping
5
5
  from typing import Any
6
6
 
7
7
  from mcp.types import ToolAnnotations
@@ -27,11 +27,15 @@ class ToolManager:
27
27
  self,
28
28
  duplicate_behavior: DuplicateBehavior | None = None,
29
29
  mask_error_details: bool | None = None,
30
- transformations: dict[str, ToolTransformConfig] | None = None,
30
+ transformations: Mapping[str, ToolTransformConfig] | None = None,
31
31
  ):
32
32
  self._tools: dict[str, Tool] = {}
33
- self.mask_error_details = mask_error_details or settings.mask_error_details
34
- self.transformations = transformations or {}
33
+ self.mask_error_details: bool = (
34
+ mask_error_details or settings.mask_error_details
35
+ )
36
+ self.transformations: dict[str, ToolTransformConfig] = dict(
37
+ transformations or {}
38
+ )
35
39
 
36
40
  # Default to "warn" if None is provided
37
41
  if duplicate_behavior is None:
fastmcp/utilities/cli.py CHANGED
@@ -150,22 +150,52 @@ _ __ ___ /_/ \____/____/\__/_/ /_/\____/_/ /_____(*)____/
150
150
  # █▀▀ ▄▀█ █▀▀ ▀█▀ █▀▄▀█ █▀▀ █▀█
151
151
  # █▀ █▀█ ▄▄█ █ █ ▀ █ █▄▄ █▀▀
152
152
  LOGO_ASCII_2 = (
153
- "\033[38;2;0;198;255m \033[38;2;0;195;255m█\033[38;2;0;192;255m▀\033[38;2;0;189;255m▀\033[38;2;0;186;255m "
154
- "\033[38;2;0;184;255m▄\033[38;2;0;181;255m▀\033[38;2;0;178;255m█\033[38;2;0;175;255m "
155
- "\033[38;2;0;172;255m█\033[38;2;0;169;255m▀\033[38;2;0;166;255m▀\033[38;2;0;163;255m "
156
- "\033[38;2;0;160;255m▀\033[38;2;0;157;255m█\033[38;2;0;155;255m▀\033[38;2;0;152;255m "
157
- "\033[38;2;0;149;255m█\033[38;2;0;146;255m▀\033[38;2;0;143;255m▄\033[38;2;0;140;255m▀\033[38;2;0;137;255m█\033[38;2;0;134;255m "
158
- "\033[38;2;0;131;255m█\033[38;2;0;128;255m▀\033[38;2;0;126;255m▀\033[38;2;0;123;255m "
159
- "\033[38;2;0;120;255m█\033[38;2;0;117;255m▀\033[38;2;0;114;255m█\033[39m\n"
160
- "\033[38;2;0;198;255m \033[38;2;0;195;255m█\033[38;2;0;192;255m▀\033[38;2;0;189;255m \033[38;2;0;186;255m "
161
- "\033[38;2;0;184;255m█\033[38;2;0;181;255m▀\033[38;2;0;178;255m█\033[38;2;0;175;255m "
162
- "\033[38;2;0;172;255m▄\033[38;2;0;169;255m▄\033[38;2;0;166;255m█\033[38;2;0;163;255m "
163
- "\033[38;2;0;160;255m \033[38;2;0;157;255m█\033[38;2;0;155;255m \033[38;2;0;152;255m "
164
- "\033[38;2;0;149;255m█\033[38;2;0;146;255m \033[38;2;0;143;255m▀\033[38;2;0;140;255m \033[38;2;0;137;255m█\033[38;2;0;134;255m "
165
- "\033[38;2;0;131;255m█\033[38;2;0;128;255m▄\033[38;2;0;126;255m▄\033[38;2;0;123;255m "
166
- "\033[38;2;0;120;255m█\033[38;2;0;117;255m▀\033[38;2;0;114;255m▀\033[39m"
153
+ "\x1b[38;2;0;198;255m \x1b[38;2;0;195;255m█\x1b[38;2;0;192;255m▀\x1b[38;2;0;189;255m▀\x1b[38;2;0;186;255m "
154
+ "\x1b[38;2;0;184;255m▄\x1b[38;2;0;181;255m▀\x1b[38;2;0;178;255m█\x1b[38;2;0;175;255m "
155
+ "\x1b[38;2;0;172;255m█\x1b[38;2;0;169;255m▀\x1b[38;2;0;166;255m▀\x1b[38;2;0;163;255m "
156
+ "\x1b[38;2;0;160;255m▀\x1b[38;2;0;157;255m█\x1b[38;2;0;155;255m▀\x1b[38;2;0;152;255m "
157
+ "\x1b[38;2;0;149;255m█\x1b[38;2;0;146;255m▀\x1b[38;2;0;143;255m▄\x1b[38;2;0;140;255m▀\x1b[38;2;0;137;255m█\x1b[38;2;0;134;255m "
158
+ "\x1b[38;2;0;131;255m█\x1b[38;2;0;128;255m▀\x1b[38;2;0;126;255m▀\x1b[38;2;0;123;255m "
159
+ "\x1b[38;2;0;120;255m█\x1b[38;2;0;117;255m▀\x1b[38;2;0;114;255m█\x1b[39m\n"
160
+ "\x1b[38;2;0;198;255m \x1b[38;2;0;195;255m█\x1b[38;2;0;192;255m▀\x1b[38;2;0;189;255m \x1b[38;2;0;186;255m "
161
+ "\x1b[38;2;0;184;255m█\x1b[38;2;0;181;255m▀\x1b[38;2;0;178;255m█\x1b[38;2;0;175;255m "
162
+ "\x1b[38;2;0;172;255m▄\x1b[38;2;0;169;255m▄\x1b[38;2;0;166;255m█\x1b[38;2;0;163;255m "
163
+ "\x1b[38;2;0;160;255m \x1b[38;2;0;157;255m█\x1b[38;2;0;155;255m \x1b[38;2;0;152;255m "
164
+ "\x1b[38;2;0;149;255m█\x1b[38;2;0;146;255m \x1b[38;2;0;143;255m▀\x1b[38;2;0;140;255m \x1b[38;2;0;137;255m█\x1b[38;2;0;134;255m "
165
+ "\x1b[38;2;0;131;255m█\x1b[38;2;0;128;255m▄\x1b[38;2;0;126;255m▄\x1b[38;2;0;123;255m "
166
+ "\x1b[38;2;0;120;255m█\x1b[38;2;0;117;255m▀\x1b[38;2;0;114;255m▀\x1b[39m"
167
167
  ).strip()
168
168
 
169
+ # Prints the below in a blue gradient - sylized F
170
+ # ▄▀▀▀
171
+ # █▀▀
172
+ # ▀
173
+ LOGO_ASCII_3 = (
174
+ " \x1b[38;2;0;170;255m▄\x1b[38;2;0;142;255m▀\x1b[38;2;0;114;255m▀\x1b[38;2;0;86;255m▀\x1b[39m\n"
175
+ " \x1b[38;2;0;170;255m█\x1b[38;2;0;142;255m▀\x1b[38;2;0;114;255m▀\x1b[39m\n"
176
+ "\x1b[38;2;0;170;255m▀\x1b[39m\n"
177
+ "\x1b[0m"
178
+ )
179
+
180
+ # Prints the below in a blue gradient - block logo with slightly stylized F
181
+ # ▄▀▀ ▄▀█ █▀▀ ▀█▀ █▀▄▀█ █▀▀ █▀█
182
+ # █▀ █▀█ ▄▄█ █ █ ▀ █ █▄▄ █▀▀
183
+
184
+ LOGO_ASCII_4 = (
185
+ "\x1b[38;2;0;198;255m \x1b[38;2;0;195;255m▄\x1b[38;2;0;192;255m▀\x1b[38;2;0;189;255m▀\x1b[38;2;0;186;255m \x1b[38;2;0;184;255m▄\x1b[38;2;0;181;255m▀\x1b[38;2;0;178;255m█\x1b[38;2;0;175;255m "
186
+ "\x1b[38;2;0;172;255m█\x1b[38;2;0;169;255m▀\x1b[38;2;0;166;255m▀\x1b[38;2;0;163;255m "
187
+ "\x1b[38;2;0;160;255m▀\x1b[38;2;0;157;255m█\x1b[38;2;0;155;255m▀\x1b[38;2;0;152;255m "
188
+ "\x1b[38;2;0;149;255m█\x1b[38;2;0;146;255m▀\x1b[38;2;0;143;255m▄\x1b[38;2;0;140;255m▀\x1b[38;2;0;137;255m█\x1b[38;2;0;134;255m "
189
+ "\x1b[38;2;0;131;255m█\x1b[38;2;0;128;255m▀\x1b[38;2;0;126;255m▀\x1b[38;2;0;123;255m "
190
+ "\x1b[38;2;0;120;255m█\x1b[38;2;0;117;255m▀\x1b[38;2;0;114;255m█\x1b[39m\n"
191
+ "\x1b[38;2;0;198;255m \x1b[38;2;0;195;255m█\x1b[38;2;0;192;255m▀\x1b[38;2;0;189;255m \x1b[38;2;0;186;255m \x1b[38;2;0;184;255m█\x1b[38;2;0;181;255m▀\x1b[38;2;0;178;255m█\x1b[38;2;0;175;255m "
192
+ "\x1b[38;2;0;172;255m▄\x1b[38;2;0;169;255m▄\x1b[38;2;0;166;255m█\x1b[38;2;0;163;255m "
193
+ "\x1b[38;2;0;160;255m \x1b[38;2;0;157;255m█\x1b[38;2;0;155;255m \x1b[38;2;0;152;255m "
194
+ "\x1b[38;2;0;149;255m█\x1b[38;2;0;146;255m \x1b[38;2;0;143;255m▀\x1b[38;2;0;140;255m \x1b[38;2;0;137;255m█\x1b[38;2;0;134;255m "
195
+ "\x1b[38;2;0;131;255m█\x1b[38;2;0;128;255m▄\x1b[38;2;0;126;255m▄\x1b[38;2;0;123;255m "
196
+ "\x1b[38;2;0;120;255m█\x1b[38;2;0;117;255m▀\x1b[38;2;0;114;255m▀\x1b[39m\n"
197
+ )
198
+
169
199
 
170
200
  def log_server_banner(
171
201
  server: FastMCP[Any],
@@ -187,7 +217,7 @@ def log_server_banner(
187
217
 
188
218
  # Create the logo text
189
219
  # Use Text with no_wrap and markup disabled to preserve ANSI escape codes
190
- logo_text = Text.from_ansi(LOGO_ASCII_2, no_wrap=True)
220
+ logo_text = Text.from_ansi(LOGO_ASCII_4, no_wrap=True)
191
221
 
192
222
  # Create the main title
193
223
  title_text = Text(f"FastMCP {fastmcp.__version__}", style="bold blue")
fastmcp/utilities/ui.py CHANGED
@@ -111,6 +111,54 @@ BUTTON_STYLES = """
111
111
  # Info box / message box styles
112
112
  INFO_BOX_STYLES = """
113
113
  .info-box {
114
+ background: #f0f9ff;
115
+ border: 1px solid #bae6fd;
116
+ border-radius: 0.5rem;
117
+ padding: 1rem;
118
+ margin-bottom: 1.5rem;
119
+ text-align: left;
120
+ font-size: 0.9375rem;
121
+ line-height: 1.5;
122
+ color: #374151;
123
+ }
124
+
125
+ .info-box p {
126
+ margin-bottom: 0.5rem;
127
+ }
128
+
129
+ .info-box p:last-child {
130
+ margin-bottom: 0;
131
+ }
132
+
133
+ .info-box.centered {
134
+ text-align: center;
135
+ }
136
+
137
+ .info-box.error {
138
+ background: #fef2f2;
139
+ border-color: #fecaca;
140
+ color: #991b1b;
141
+ }
142
+
143
+ .info-box strong {
144
+ color: #0ea5e9;
145
+ font-weight: 600;
146
+ }
147
+
148
+ .info-box .server-name-link {
149
+ color: #0ea5e9;
150
+ text-decoration: underline;
151
+ font-weight: 600;
152
+ cursor: pointer;
153
+ transition: opacity 0.15s;
154
+ }
155
+
156
+ .info-box .server-name-link:hover {
157
+ opacity: 0.8;
158
+ }
159
+
160
+ /* Monospace info box - gray styling with code font */
161
+ .info-box-mono {
114
162
  background: #f9fafb;
115
163
  border: 1px solid #e5e7eb;
116
164
  border-radius: 0.5rem;
@@ -122,17 +170,17 @@ INFO_BOX_STYLES = """
122
170
  text-align: left;
123
171
  }
124
172
 
125
- .info-box.centered {
173
+ .info-box-mono.centered {
126
174
  text-align: center;
127
175
  }
128
176
 
129
- .info-box.error {
177
+ .info-box-mono.error {
130
178
  background: #fef2f2;
131
179
  border-color: #fecaca;
132
180
  color: #991b1b;
133
181
  }
134
182
 
135
- .info-box strong {
183
+ .info-box-mono strong {
136
184
  color: #111827;
137
185
  font-weight: 600;
138
186
  }
@@ -236,10 +284,11 @@ DETAIL_BOX_STYLES = """
236
284
 
237
285
  .detail-label {
238
286
  font-weight: 600;
239
- min-width: 140px;
287
+ min-width: 160px;
240
288
  color: #6b7280;
241
289
  font-size: 0.875rem;
242
290
  flex-shrink: 0;
291
+ padding-right: 1rem;
243
292
  }
244
293
 
245
294
  .detail-value {
@@ -252,6 +301,72 @@ DETAIL_BOX_STYLES = """
252
301
  }
253
302
  """
254
303
 
304
+ # Redirect section styles (for OAuth redirect URI box)
305
+ REDIRECT_SECTION_STYLES = """
306
+ .redirect-section {
307
+ background: #fffbeb;
308
+ border: 1px solid #fcd34d;
309
+ border-radius: 0.5rem;
310
+ padding: 1rem;
311
+ margin-bottom: 1.5rem;
312
+ text-align: left;
313
+ }
314
+
315
+ .redirect-section .label {
316
+ font-size: 0.875rem;
317
+ color: #6b7280;
318
+ font-weight: 600;
319
+ margin-bottom: 0.5rem;
320
+ display: block;
321
+ }
322
+
323
+ .redirect-section .value {
324
+ font-family: 'SF Mono', 'Monaco', 'Consolas', 'Courier New', monospace;
325
+ font-size: 0.875rem;
326
+ color: #111827;
327
+ word-break: break-all;
328
+ margin-top: 0.25rem;
329
+ }
330
+ """
331
+
332
+ # Collapsible details styles
333
+ DETAILS_STYLES = """
334
+ details {
335
+ margin-bottom: 1.5rem;
336
+ text-align: left;
337
+ }
338
+
339
+ summary {
340
+ cursor: pointer;
341
+ font-size: 0.875rem;
342
+ color: #6b7280;
343
+ font-weight: 600;
344
+ list-style: none;
345
+ padding: 0.5rem;
346
+ border-radius: 0.25rem;
347
+ }
348
+
349
+ summary:hover {
350
+ background: #f9fafb;
351
+ }
352
+
353
+ summary::marker {
354
+ display: none;
355
+ }
356
+
357
+ summary::before {
358
+ content: "▶";
359
+ display: inline-block;
360
+ margin-right: 0.5rem;
361
+ transition: transform 0.2s;
362
+ font-size: 0.75rem;
363
+ }
364
+
365
+ details[open] summary::before {
366
+ transform: rotate(90deg);
367
+ }
368
+ """
369
+
255
370
  # Helper text styles
256
371
  HELPER_TEXT_STYLES = """
257
372
  .close-instruction, .help-text {
@@ -413,7 +528,10 @@ def create_status_message(message: str, is_success: bool = True) -> str:
413
528
 
414
529
 
415
530
  def create_info_box(
416
- content: str, is_error: bool = False, centered: bool = False
531
+ content: str,
532
+ is_error: bool = False,
533
+ centered: bool = False,
534
+ monospace: bool = False,
417
535
  ) -> str:
418
536
  """
419
537
  Create an info box.
@@ -422,12 +540,14 @@ def create_info_box(
422
540
  content: HTML content for the info box
423
541
  is_error: True for error styling, False for normal
424
542
  centered: True to center the text, False for left-aligned
543
+ monospace: True to use gray monospace font styling instead of blue
425
544
 
426
545
  Returns:
427
546
  HTML for info box
428
547
  """
429
548
  content = html.escape(content)
430
- classes = ["info-box"]
549
+ base_class = "info-box-mono" if monospace else "info-box"
550
+ classes = [base_class]
431
551
  if is_error:
432
552
  classes.append("error")
433
553
  if centered:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastmcp
3
- Version: 2.13.0rc2
3
+ Version: 2.13.0rc3
4
4
  Summary: The fast, Pythonic way to build MCP servers and clients.
5
5
  Project-URL: Homepage, https://gofastmcp.com
6
6
  Project-URL: Repository, https://github.com/jlowin/fastmcp
@@ -14,6 +14,7 @@ Classifier: License :: OSI Approved :: Apache Software License
14
14
  Classifier: Programming Language :: Python :: 3.10
15
15
  Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
17
18
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
19
  Classifier: Typing :: Typed
19
20
  Requires-Python: >=3.10
@@ -24,10 +25,10 @@ Requires-Dist: httpx>=0.28.1
24
25
  Requires-Dist: mcp<2.0.0,>=1.17.0
25
26
  Requires-Dist: openapi-core>=0.19.5
26
27
  Requires-Dist: openapi-pydantic>=0.5.1
27
- Requires-Dist: py-key-value-aio[disk,memory]<0.3.0,>=0.2.2
28
+ Requires-Dist: platformdirs>=4.0.0
29
+ Requires-Dist: py-key-value-aio[disk,keyring,memory]<0.3.0,>=0.2.6
28
30
  Requires-Dist: pydantic[email]>=2.11.7
29
31
  Requires-Dist: pyperclip>=1.9.0
30
- Requires-Dist: pytest-asyncio>=1.2.0
31
32
  Requires-Dist: python-dotenv>=1.1.0
32
33
  Requires-Dist: rich>=13.9.4
33
34
  Requires-Dist: websockets>=15.0.1
@@ -248,7 +249,6 @@ Access MCP session capabilities within your tools, resources, or prompts by addi
248
249
 
249
250
  - **Logging:** Log messages to MCP clients with `ctx.info()`, `ctx.error()`, etc.
250
251
  - **LLM Sampling:** Use `ctx.sample()` to request completions from the client's LLM.
251
- - **HTTP Request:** Use `ctx.http_request()` to make HTTP requests to other servers.
252
252
  - **Resource Access:** Use `ctx.read_resource()` to access resources on the server
253
253
  - **Progress Reporting:** Use `ctx.report_progress()` to report progress to the client.
254
254
  - and more...
@@ -358,7 +358,7 @@ FastMCP provides comprehensive authentication support that sets it apart from ba
358
358
  Protecting a server takes just two lines:
359
359
 
360
360
  ```python
361
- from fastmcp.server.auth import GoogleProvider
361
+ from fastmcp.server.auth.providers.google import GoogleProvider
362
362
 
363
363
  auth = GoogleProvider(client_id="...", client_secret="...", base_url="https://myserver.com")
364
364
  mcp = FastMCP("Protected Server", auth=auth)