fastmcp 2.13.0rc1__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
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  import os
5
- from importlib.metadata import version
6
5
  from pathlib import Path
7
6
  from typing import TYPE_CHECKING, Any, Literal
8
7
 
@@ -138,7 +137,7 @@ def load_and_merge_config(
138
137
  return new_config, resolved_spec
139
138
 
140
139
 
141
- LOGO_ASCII = r"""
140
+ LOGO_ASCII_1 = r"""
142
141
  _ __ ___ _____ __ __ _____________ ____ ____
143
142
  _ __ ___ .'____/___ ______/ /_/ |/ / ____/ __ \ |___ \ / __ \
144
143
  _ __ ___ / /_ / __ `/ ___/ __/ /|_/ / / / /_/ / ___/ / / / / /
@@ -147,6 +146,56 @@ _ __ ___ /_/ \____/____/\__/_/ /_/\____/_/ /_____(*)____/
147
146
 
148
147
  """.lstrip("\n")
149
148
 
149
+ # This prints the below in a blue gradient
150
+ # █▀▀ ▄▀█ █▀▀ ▀█▀ █▀▄▀█ █▀▀ █▀█
151
+ # █▀ █▀█ ▄▄█ █ █ ▀ █ █▄▄ █▀▀
152
+ LOGO_ASCII_2 = (
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
+ ).strip()
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
+
150
199
 
151
200
  def log_server_banner(
152
201
  server: FastMCP[Any],
@@ -167,10 +216,11 @@ def log_server_banner(
167
216
  """
168
217
 
169
218
  # Create the logo text
170
- logo_text = Text(LOGO_ASCII, style="bold green")
219
+ # Use Text with no_wrap and markup disabled to preserve ANSI escape codes
220
+ logo_text = Text.from_ansi(LOGO_ASCII_4, no_wrap=True)
171
221
 
172
222
  # Create the main title
173
- title_text = Text("FastMCP 2.0", style="bold blue")
223
+ title_text = Text(f"FastMCP {fastmcp.__version__}", style="bold blue")
174
224
 
175
225
  # Create the information table
176
226
  info_table = Table.grid(padding=(0, 1))
@@ -180,13 +230,13 @@ def log_server_banner(
180
230
 
181
231
  match transport:
182
232
  case "http" | "streamable-http":
183
- display_transport = "Streamable-HTTP"
233
+ display_transport = "HTTP"
184
234
  case "sse":
185
235
  display_transport = "SSE"
186
236
  case "stdio":
187
237
  display_transport = "STDIO"
188
238
 
189
- info_table.add_row("🖥", "Server name:", server.name)
239
+ info_table.add_row("🖥", "Server name:", Text(server.name + "\n", style="bold blue"))
190
240
  info_table.add_row("📦", "Transport:", display_transport)
191
241
 
192
242
  # Show connection info based on transport
@@ -197,27 +247,15 @@ def log_server_banner(
197
247
  server_url += f"/{path.lstrip('/')}"
198
248
  info_table.add_row("🔗", "Server URL:", server_url)
199
249
 
200
- # Add version information with explicit style overrides
201
- info_table.add_row("", "", "")
202
- info_table.add_row(
203
- "🏎",
204
- "FastMCP version:",
205
- Text(fastmcp.__version__, style="dim white", no_wrap=True),
206
- )
207
- info_table.add_row(
208
- "🤝",
209
- "MCP SDK version:",
210
- Text(version("mcp"), style="dim white", no_wrap=True),
211
- )
212
-
213
250
  # Add documentation link
214
251
  info_table.add_row("", "", "")
215
252
  info_table.add_row("📚", "Docs:", "https://gofastmcp.com")
216
- info_table.add_row("🚀", "Deploy:", "https://fastmcp.cloud")
253
+ info_table.add_row("🚀", "Hosting:", "https://fastmcp.cloud")
217
254
 
218
255
  # Create panel with logo, title, and information using Group
219
256
  panel_content = Group(
220
257
  Align.center(logo_text),
258
+ "",
221
259
  Align.center(title_text),
222
260
  "",
223
261
  "",
@@ -228,8 +266,10 @@ def log_server_banner(
228
266
  panel_content,
229
267
  border_style="dim",
230
268
  padding=(1, 4),
231
- expand=False,
269
+ # expand=False,
270
+ width=80, # Set max width for the panel
232
271
  )
233
272
 
234
273
  console = Console(stderr=True)
235
- console.print(Group("\n", panel, "\n"))
274
+ # Center the panel itself
275
+ console.print(Group("\n", Align.center(panel), "\n"))
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: