fastmcp 2.11.2__py3-none-any.whl → 2.12.0rc1__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.
Files changed (77) hide show
  1. fastmcp/__init__.py +5 -4
  2. fastmcp/cli/claude.py +22 -18
  3. fastmcp/cli/cli.py +472 -136
  4. fastmcp/cli/install/claude_code.py +37 -40
  5. fastmcp/cli/install/claude_desktop.py +37 -42
  6. fastmcp/cli/install/cursor.py +148 -38
  7. fastmcp/cli/install/mcp_json.py +38 -43
  8. fastmcp/cli/install/shared.py +64 -7
  9. fastmcp/cli/run.py +122 -215
  10. fastmcp/client/auth/oauth.py +69 -13
  11. fastmcp/client/client.py +46 -9
  12. fastmcp/client/logging.py +25 -1
  13. fastmcp/client/oauth_callback.py +91 -91
  14. fastmcp/client/sampling.py +12 -4
  15. fastmcp/client/transports.py +143 -67
  16. fastmcp/experimental/sampling/__init__.py +0 -0
  17. fastmcp/experimental/sampling/handlers/__init__.py +3 -0
  18. fastmcp/experimental/sampling/handlers/base.py +21 -0
  19. fastmcp/experimental/sampling/handlers/openai.py +163 -0
  20. fastmcp/experimental/server/openapi/routing.py +1 -3
  21. fastmcp/experimental/server/openapi/server.py +10 -25
  22. fastmcp/experimental/utilities/openapi/__init__.py +2 -2
  23. fastmcp/experimental/utilities/openapi/formatters.py +34 -0
  24. fastmcp/experimental/utilities/openapi/models.py +5 -2
  25. fastmcp/experimental/utilities/openapi/parser.py +252 -70
  26. fastmcp/experimental/utilities/openapi/schemas.py +135 -106
  27. fastmcp/mcp_config.py +40 -20
  28. fastmcp/prompts/prompt_manager.py +4 -2
  29. fastmcp/resources/resource_manager.py +16 -6
  30. fastmcp/server/auth/__init__.py +11 -1
  31. fastmcp/server/auth/auth.py +19 -2
  32. fastmcp/server/auth/oauth_proxy.py +1047 -0
  33. fastmcp/server/auth/providers/azure.py +270 -0
  34. fastmcp/server/auth/providers/github.py +287 -0
  35. fastmcp/server/auth/providers/google.py +305 -0
  36. fastmcp/server/auth/providers/jwt.py +27 -16
  37. fastmcp/server/auth/providers/workos.py +256 -2
  38. fastmcp/server/auth/redirect_validation.py +65 -0
  39. fastmcp/server/auth/registry.py +1 -1
  40. fastmcp/server/context.py +91 -41
  41. fastmcp/server/dependencies.py +32 -2
  42. fastmcp/server/elicitation.py +60 -1
  43. fastmcp/server/http.py +44 -37
  44. fastmcp/server/middleware/logging.py +66 -28
  45. fastmcp/server/proxy.py +2 -0
  46. fastmcp/server/sampling/handler.py +19 -0
  47. fastmcp/server/server.py +85 -20
  48. fastmcp/settings.py +18 -3
  49. fastmcp/tools/tool.py +23 -10
  50. fastmcp/tools/tool_manager.py +5 -1
  51. fastmcp/tools/tool_transform.py +75 -32
  52. fastmcp/utilities/auth.py +34 -0
  53. fastmcp/utilities/cli.py +148 -15
  54. fastmcp/utilities/components.py +21 -5
  55. fastmcp/utilities/inspect.py +166 -37
  56. fastmcp/utilities/json_schema_type.py +4 -2
  57. fastmcp/utilities/logging.py +4 -1
  58. fastmcp/utilities/mcp_config.py +47 -18
  59. fastmcp/utilities/mcp_server_config/__init__.py +25 -0
  60. fastmcp/utilities/mcp_server_config/v1/__init__.py +0 -0
  61. fastmcp/utilities/mcp_server_config/v1/environments/__init__.py +6 -0
  62. fastmcp/utilities/mcp_server_config/v1/environments/base.py +30 -0
  63. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +306 -0
  64. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +446 -0
  65. fastmcp/utilities/mcp_server_config/v1/schema.json +361 -0
  66. fastmcp/utilities/mcp_server_config/v1/sources/__init__.py +0 -0
  67. fastmcp/utilities/mcp_server_config/v1/sources/base.py +30 -0
  68. fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py +216 -0
  69. fastmcp/utilities/openapi.py +4 -4
  70. fastmcp/utilities/tests.py +7 -2
  71. fastmcp/utilities/types.py +15 -2
  72. {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/METADATA +3 -2
  73. fastmcp-2.12.0rc1.dist-info/RECORD +129 -0
  74. fastmcp-2.11.2.dist-info/RECORD +0 -108
  75. {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/WHEEL +0 -0
  76. {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/entry_points.txt +0 -0
  77. {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/licenses/LICENSE +0 -0
@@ -6,7 +6,7 @@ import os
6
6
  import shutil
7
7
  import sys
8
8
  import warnings
9
- from collections.abc import AsyncIterator, Callable
9
+ from collections.abc import AsyncIterator
10
10
  from pathlib import Path
11
11
  from typing import Any, Literal, TypeVar, cast, overload
12
12
 
@@ -21,7 +21,11 @@ from mcp.client.session import (
21
21
  MessageHandlerFnT,
22
22
  SamplingFnT,
23
23
  )
24
+ from mcp.client.sse import sse_client
25
+ from mcp.client.stdio import stdio_client
26
+ from mcp.client.streamable_http import streamablehttp_client
24
27
  from mcp.server.fastmcp import FastMCP as FastMCP1Server
28
+ from mcp.shared._httpx_utils import McpHttpClientFactory
25
29
  from mcp.shared.memory import create_client_server_memory_streams
26
30
  from pydantic import AnyUrl
27
31
  from typing_extensions import TypedDict, Unpack
@@ -33,6 +37,7 @@ from fastmcp.mcp_config import MCPConfig, infer_transport_type_from_url
33
37
  from fastmcp.server.dependencies import get_http_headers
34
38
  from fastmcp.server.server import FastMCP
35
39
  from fastmcp.utilities.logging import get_logger
40
+ from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
36
41
 
37
42
  logger = get_logger(__name__)
38
43
 
@@ -161,7 +166,7 @@ class SSETransport(ClientTransport):
161
166
  headers: dict[str, str] | None = None,
162
167
  auth: httpx.Auth | Literal["oauth"] | str | None = None,
163
168
  sse_read_timeout: datetime.timedelta | float | int | None = None,
164
- httpx_client_factory: Callable[[], httpx.AsyncClient] | None = None,
169
+ httpx_client_factory: McpHttpClientFactory | None = None,
165
170
  ):
166
171
  if isinstance(url, AnyUrl):
167
172
  url = str(url)
@@ -177,7 +182,7 @@ class SSETransport(ClientTransport):
177
182
  self.httpx_client_factory = httpx_client_factory
178
183
 
179
184
  if isinstance(sse_read_timeout, int | float):
180
- sse_read_timeout = datetime.timedelta(seconds=sse_read_timeout)
185
+ sse_read_timeout = datetime.timedelta(seconds=float(sse_read_timeout))
181
186
  self.sse_read_timeout = sse_read_timeout
182
187
 
183
188
  def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
@@ -191,8 +196,6 @@ class SSETransport(ClientTransport):
191
196
  async def connect_session(
192
197
  self, **session_kwargs: Unpack[SessionKwargs]
193
198
  ) -> AsyncIterator[ClientSession]:
194
- from mcp.client.sse import sse_client
195
-
196
199
  client_kwargs: dict[str, Any] = {}
197
200
 
198
201
  # load headers from an active HTTP request, if available. This will only be true
@@ -233,7 +236,7 @@ class StreamableHttpTransport(ClientTransport):
233
236
  headers: dict[str, str] | None = None,
234
237
  auth: httpx.Auth | Literal["oauth"] | str | None = None,
235
238
  sse_read_timeout: datetime.timedelta | float | int | None = None,
236
- httpx_client_factory: Callable[[], httpx.AsyncClient] | None = None,
239
+ httpx_client_factory: McpHttpClientFactory | None = None,
237
240
  ):
238
241
  if isinstance(url, AnyUrl):
239
242
  url = str(url)
@@ -249,7 +252,7 @@ class StreamableHttpTransport(ClientTransport):
249
252
  self.httpx_client_factory = httpx_client_factory
250
253
 
251
254
  if isinstance(sse_read_timeout, int | float):
252
- sse_read_timeout = datetime.timedelta(seconds=sse_read_timeout)
255
+ sse_read_timeout = datetime.timedelta(seconds=float(sse_read_timeout))
253
256
  self.sse_read_timeout = sse_read_timeout
254
257
 
255
258
  def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
@@ -263,8 +266,6 @@ class StreamableHttpTransport(ClientTransport):
263
266
  async def connect_session(
264
267
  self, **session_kwargs: Unpack[SessionKwargs]
265
268
  ) -> AsyncIterator[ClientSession]:
266
- from mcp.client.streamable_http import streamablehttp_client
267
-
268
269
  client_kwargs: dict[str, Any] = {}
269
270
 
270
271
  # load headers from an active HTTP request, if available. This will only be true
@@ -345,8 +346,7 @@ class StdioTransport(ClientTransport):
345
346
  ) -> AsyncIterator[ClientSession]:
346
347
  try:
347
348
  await self.connect(**session_kwargs)
348
- assert self._session is not None
349
- yield self._session
349
+ yield cast(ClientSession, self._session)
350
350
  finally:
351
351
  if not self.keep_alive:
352
352
  await self.disconnect()
@@ -359,42 +359,22 @@ class StdioTransport(ClientTransport):
359
359
  if self._connect_task is not None:
360
360
  return
361
361
 
362
- async def _connect_task():
363
- from mcp.client.stdio import stdio_client
364
-
365
- try:
366
- async with contextlib.AsyncExitStack() as stack:
367
- try:
368
- server_params = StdioServerParameters(
369
- command=self.command,
370
- args=self.args,
371
- env=self.env,
372
- cwd=self.cwd,
373
- )
374
- transport = await stack.enter_async_context(
375
- stdio_client(server_params)
376
- )
377
- read_stream, write_stream = transport
378
- self._session = await stack.enter_async_context(
379
- ClientSession(read_stream, write_stream, **session_kwargs)
380
- )
381
-
382
- logger.debug("Stdio transport connected")
383
- self._ready_event.set()
384
-
385
- # Wait until disconnect is requested (stop_event is set)
386
- await self._stop_event.wait()
387
- finally:
388
- # Clean up client on exit
389
- self._session = None
390
- logger.debug("Stdio transport disconnected")
391
- except Exception:
392
- # Ensure ready event is set even if connection fails
393
- self._ready_event.set()
394
- raise
362
+ session_future: asyncio.Future[ClientSession] = asyncio.Future()
395
363
 
396
364
  # start the connection task
397
- self._connect_task = asyncio.create_task(_connect_task())
365
+ self._connect_task = asyncio.create_task(
366
+ _stdio_transport_connect_task(
367
+ command=self.command,
368
+ args=self.args,
369
+ env=self.env,
370
+ cwd=self.cwd,
371
+ session_kwargs=session_kwargs,
372
+ ready_event=self._ready_event,
373
+ stop_event=self._stop_event,
374
+ session_future=session_future,
375
+ )
376
+ )
377
+
398
378
  # wait for the client to be ready before returning
399
379
  await self._ready_event.wait()
400
380
 
@@ -404,6 +384,9 @@ class StdioTransport(ClientTransport):
404
384
  if exception is not None:
405
385
  raise exception
406
386
 
387
+ self._session = await session_future
388
+ return self._session
389
+
407
390
  async def disconnect(self):
408
391
  if self._connect_task is None:
409
392
  return
@@ -422,12 +405,61 @@ class StdioTransport(ClientTransport):
422
405
  async def close(self):
423
406
  await self.disconnect()
424
407
 
408
+ def __del__(self):
409
+ """Ensure that we send a disconnection signal to the transport task if we are being garbage collected."""
410
+ if not self._stop_event.is_set():
411
+ self._stop_event.set()
412
+
425
413
  def __repr__(self) -> str:
426
414
  return (
427
415
  f"<{self.__class__.__name__}(command='{self.command}', args={self.args})>"
428
416
  )
429
417
 
430
418
 
419
+ async def _stdio_transport_connect_task(
420
+ command: str,
421
+ args: list[str],
422
+ env: dict[str, str] | None,
423
+ cwd: str | None,
424
+ session_kwargs: SessionKwargs,
425
+ ready_event: anyio.Event,
426
+ stop_event: anyio.Event,
427
+ session_future: asyncio.Future[ClientSession],
428
+ ):
429
+ """A standalone connection task for a stdio transport. It is not a part of the StdioTransport class
430
+ to ensure that the connection task does not hold a reference to the Transport object."""
431
+
432
+ try:
433
+ async with contextlib.AsyncExitStack() as stack:
434
+ try:
435
+ server_params = StdioServerParameters(
436
+ command=command,
437
+ args=args,
438
+ env=env,
439
+ cwd=cwd,
440
+ )
441
+ transport = await stack.enter_async_context(stdio_client(server_params))
442
+ read_stream, write_stream = transport
443
+ session_future.set_result(
444
+ await stack.enter_async_context(
445
+ ClientSession(read_stream, write_stream, **session_kwargs)
446
+ )
447
+ )
448
+
449
+ logger.debug("Stdio transport connected")
450
+ ready_event.set()
451
+
452
+ # Wait until disconnect is requested (stop_event is set)
453
+ await stop_event.wait()
454
+ finally:
455
+ # Clean up client on exit
456
+ logger.debug("Stdio transport disconnected")
457
+ except Exception:
458
+ # Ensure ready event is set even if connection fails
459
+ ready_event.set()
460
+ raise
461
+
462
+
431
463
  class PythonStdioTransport(StdioTransport):
432
464
  """Transport for running Python scripts."""
433
465
 
@@ -564,16 +596,38 @@ class UvStdioTransport(StdioTransport):
564
596
  f"Project directory not found: {project_directory}"
565
597
  )
566
598
 
567
- # Build uv arguments
568
- uv_args: list[str] = ["run"]
569
- if project_directory:
570
- uv_args.extend(["--directory", str(project_directory)])
571
- if python_version:
572
- uv_args.extend(["--python", python_version])
573
- for pkg in with_packages or []:
574
- uv_args.extend(["--with", pkg])
575
- if with_requirements:
576
- uv_args.extend(["--with-requirements", str(with_requirements)])
599
+ # Create Environment from provided parameters (internal use)
600
+ env_config = UVEnvironment(
601
+ python=python_version,
602
+ dependencies=with_packages,
603
+ requirements=with_requirements,
604
+ project=project_directory,
605
+ editable=None, # Not exposed in this transport
606
+ )
607
+
608
+ # Build uv arguments using the config
609
+ uv_args: list[str] = []
610
+
611
+ # Check if we need any environment setup
612
+ if env_config.needs_uv():
613
+ # Use the config to build args, but we need to handle the command differently
614
+ # since transport has specific needs
615
+ uv_args = ["run"]
616
+
617
+ if python_version:
618
+ uv_args.extend(["--python", python_version])
619
+ if project_directory:
620
+ uv_args.extend(["--directory", str(project_directory)])
621
+
622
+ # Note: Don't add fastmcp as dependency here, transport is for general use
623
+ for pkg in with_packages or []:
624
+ uv_args.extend(["--with", pkg])
625
+ if with_requirements:
626
+ uv_args.extend(["--with-requirements", str(with_requirements)])
627
+ else:
628
+ # No environment setup needed
629
+ uv_args = ["run"]
630
+
577
631
  if module:
578
632
  uv_args.append("--module")
579
633
 
@@ -829,12 +883,14 @@ class MCPConfigTransport(ClientTransport):
829
883
  """
830
884
 
831
885
  def __init__(self, config: MCPConfig | dict, name_as_prefix: bool = True):
832
- from fastmcp.utilities.mcp_config import composite_server_from_mcp_config
886
+ from fastmcp.utilities.mcp_config import mcp_config_to_servers_and_transports
833
887
 
834
888
  if isinstance(config, dict):
835
889
  config = MCPConfig.from_dict(config)
836
890
  self.config = config
837
891
 
892
+ self._underlying_transports: list[ClientTransport] = []
893
+
838
894
  # if there are no servers, raise an error
839
895
  if len(self.config.mcpServers) == 0:
840
896
  raise ValueError("No MCP servers defined in the config")
@@ -842,14 +898,22 @@ class MCPConfigTransport(ClientTransport):
842
898
  # if there's exactly one server, create a client for that server
843
899
  elif len(self.config.mcpServers) == 1:
844
900
  self.transport = list(self.config.mcpServers.values())[0].to_transport()
901
+ self._underlying_transports.append(self.transport)
845
902
 
846
903
  # otherwise create a composite client
847
904
  else:
848
- self.transport = FastMCPTransport(
849
- mcp=composite_server_from_mcp_config(
850
- self.config, name_as_prefix=name_as_prefix
905
+ name = FastMCP.generate_name("MCPRouter")
906
+ self._composite_server = FastMCP[Any](name=name)
907
+
908
+ for name, server, transport in mcp_config_to_servers_and_transports(
909
+ self.config
910
+ ):
911
+ self._underlying_transports.append(transport)
912
+ self._composite_server.mount(
913
+ server, prefix=name if name_as_prefix else None
851
914
  )
852
- )
915
+
916
+ self.transport = FastMCPTransport(mcp=self._composite_server)
853
917
 
854
918
  @contextlib.asynccontextmanager
855
919
  async def connect_session(
@@ -858,6 +922,10 @@ class MCPConfigTransport(ClientTransport):
858
922
  async with self.transport.connect_session(**session_kwargs) as session:
859
923
  yield session
860
924
 
925
+ async def close(self):
926
+ for transport in self._underlying_transports:
927
+ await transport.close()
928
+
861
929
  def __repr__(self) -> str:
862
930
  return f"<MCPConfigTransport(config='{self.config}')>"
863
931
 
@@ -957,28 +1025,36 @@ def infer_transport(
957
1025
 
958
1026
  # the transport is a FastMCP server (2.x or 1.0)
959
1027
  elif isinstance(transport, FastMCP | FastMCP1Server):
960
- inferred_transport = FastMCPTransport(mcp=transport)
1028
+ inferred_transport = FastMCPTransport(
1029
+ mcp=cast(FastMCP[Any] | FastMCP1Server, transport)
1030
+ )
961
1031
 
962
1032
  # the transport is a path to a script
963
1033
  elif isinstance(transport, Path | str) and Path(transport).exists():
964
1034
  if str(transport).endswith(".py"):
965
- inferred_transport = PythonStdioTransport(script_path=transport)
1035
+ inferred_transport = PythonStdioTransport(script_path=cast(Path, transport))
966
1036
  elif str(transport).endswith(".js"):
967
- inferred_transport = NodeStdioTransport(script_path=transport)
1037
+ inferred_transport = NodeStdioTransport(script_path=cast(Path, transport))
968
1038
  else:
969
1039
  raise ValueError(f"Unsupported script type: {transport}")
970
1040
 
971
1041
  # the transport is an http(s) URL
972
1042
  elif isinstance(transport, AnyUrl | str) and str(transport).startswith("http"):
973
- inferred_transport_type = infer_transport_type_from_url(transport)
1043
+ inferred_transport_type = infer_transport_type_from_url(
1044
+ cast(AnyUrl | str, transport)
1045
+ )
974
1046
  if inferred_transport_type == "sse":
975
- inferred_transport = SSETransport(url=transport)
1047
+ inferred_transport = SSETransport(url=cast(AnyUrl | str, transport))
976
1048
  else:
977
- inferred_transport = StreamableHttpTransport(url=transport)
1049
+ inferred_transport = StreamableHttpTransport(
1050
+ url=cast(AnyUrl | str, transport)
1051
+ )
978
1052
 
979
1053
  # if the transport is a config dict or MCPConfig
980
1054
  elif isinstance(transport, dict | MCPConfig):
981
- inferred_transport = MCPConfigTransport(config=transport)
1055
+ inferred_transport = MCPConfigTransport(
1056
+ config=cast(dict | MCPConfig, transport)
1057
+ )
982
1058
 
983
1059
  # the transport is an unknown type
984
1060
  else:
File without changes
@@ -0,0 +1,3 @@
1
+ from .openai import OpenAISamplingHandler
2
+
3
+ __all__ = ["OpenAISamplingHandler"]
@@ -0,0 +1,21 @@
1
+ from abc import ABC, abstractmethod
2
+ from collections.abc import Awaitable
3
+
4
+ from mcp import ClientSession, CreateMessageResult
5
+ from mcp.server.session import ServerSession
6
+ from mcp.shared.context import LifespanContextT, RequestContext
7
+ from mcp.types import CreateMessageRequestParams as SamplingParams
8
+ from mcp.types import (
9
+ SamplingMessage,
10
+ )
11
+
12
+
13
+ class BaseLLMSamplingHandler(ABC):
14
+ @abstractmethod
15
+ def __call__(
16
+ self,
17
+ messages: list[SamplingMessage],
18
+ params: SamplingParams,
19
+ context: RequestContext[ServerSession, LifespanContextT]
20
+ | RequestContext[ClientSession, LifespanContextT],
21
+ ) -> str | CreateMessageResult | Awaitable[str | CreateMessageResult]: ...
@@ -0,0 +1,163 @@
1
+ from collections.abc import Iterator, Sequence
2
+ from typing import get_args
3
+
4
+ from mcp import ClientSession, ServerSession
5
+ from mcp.shared.context import LifespanContextT, RequestContext
6
+ from mcp.types import CreateMessageRequestParams as SamplingParams
7
+ from mcp.types import (
8
+ CreateMessageResult,
9
+ ModelPreferences,
10
+ SamplingMessage,
11
+ TextContent,
12
+ )
13
+ from openai import NOT_GIVEN, OpenAI
14
+ from openai.types.chat import (
15
+ ChatCompletion,
16
+ ChatCompletionAssistantMessageParam,
17
+ ChatCompletionMessageParam,
18
+ ChatCompletionSystemMessageParam,
19
+ ChatCompletionUserMessageParam,
20
+ )
21
+ from openai.types.shared.chat_model import ChatModel
22
+ from typing_extensions import override
23
+
24
+ from fastmcp.experimental.sampling.handlers.base import BaseLLMSamplingHandler
25
+
26
+
27
+ class OpenAISamplingHandler(BaseLLMSamplingHandler):
28
+ def __init__(self, default_model: ChatModel, client: OpenAI | None = None):
29
+ self.client: OpenAI = client or OpenAI()
30
+ self.default_model: ChatModel = default_model
31
+
32
+ @override
33
+ async def __call__(
34
+ self,
35
+ messages: list[SamplingMessage],
36
+ params: SamplingParams,
37
+ context: RequestContext[ServerSession, LifespanContextT]
38
+ | RequestContext[ClientSession, LifespanContextT],
39
+ ) -> CreateMessageResult:
40
+ openai_messages: list[ChatCompletionMessageParam] = (
41
+ self._convert_to_openai_messages(
42
+ system_prompt=params.systemPrompt,
43
+ messages=messages,
44
+ )
45
+ )
46
+
47
+ model: ChatModel = self._select_model_from_preferences(params.modelPreferences)
48
+
49
+ response = self.client.chat.completions.create(
50
+ model=model,
51
+ messages=openai_messages,
52
+ temperature=params.temperature or NOT_GIVEN,
53
+ max_tokens=params.maxTokens,
54
+ stop=params.stopSequences or NOT_GIVEN,
55
+ )
56
+
57
+ return self._chat_completion_to_create_message_result(response)
58
+
59
+ @staticmethod
60
+ def _iter_models_from_preferences(
61
+ model_preferences: ModelPreferences | str | list[str] | None,
62
+ ) -> Iterator[str]:
63
+ if model_preferences is None:
64
+ return
65
+
66
+ if isinstance(model_preferences, str) and model_preferences in get_args(
67
+ ChatModel
68
+ ):
69
+ yield model_preferences
70
+
71
+ if isinstance(model_preferences, list):
72
+ yield from model_preferences
73
+
74
+ if isinstance(model_preferences, ModelPreferences):
75
+ if not (hints := model_preferences.hints):
76
+ return
77
+
78
+ for hint in hints:
79
+ if not (name := hint.name):
80
+ continue
81
+
82
+ yield name
83
+
84
+ @staticmethod
85
+ def _convert_to_openai_messages(
86
+ system_prompt: str | None, messages: Sequence[SamplingMessage]
87
+ ) -> list[ChatCompletionMessageParam]:
88
+ openai_messages: list[ChatCompletionMessageParam] = []
89
+
90
+ if system_prompt:
91
+ openai_messages.append(
92
+ ChatCompletionSystemMessageParam(
93
+ role="system",
94
+ content=system_prompt,
95
+ )
96
+ )
97
+
98
+ if isinstance(messages, str):
99
+ openai_messages.append(
100
+ ChatCompletionUserMessageParam(
101
+ role="user",
102
+ content=messages,
103
+ )
104
+ )
105
+
106
+ if isinstance(messages, list):
107
+ for message in messages:
108
+ if isinstance(message, str):
109
+ openai_messages.append(
110
+ ChatCompletionUserMessageParam(
111
+ role="user",
112
+ content=message,
113
+ )
114
+ )
115
+ continue
116
+
117
+ if not isinstance(message.content, TextContent):
118
+ raise ValueError("Only text content is supported")
119
+
120
+ if message.role == "user":
121
+ openai_messages.append(
122
+ ChatCompletionUserMessageParam(
123
+ role="user",
124
+ content=message.content.text,
125
+ )
126
+ )
127
+ else:
128
+ openai_messages.append(
129
+ ChatCompletionAssistantMessageParam(
130
+ role="assistant",
131
+ content=message.content.text,
132
+ )
133
+ )
134
+
135
+ return openai_messages
136
+
137
+ @staticmethod
138
+ def _chat_completion_to_create_message_result(
139
+ chat_completion: ChatCompletion,
140
+ ) -> CreateMessageResult:
141
+ if len(chat_completion.choices) == 0:
142
+ raise ValueError("No response for completion")
143
+
144
+ first_choice = chat_completion.choices[0]
145
+
146
+ if content := first_choice.message.content:
147
+ return CreateMessageResult(
148
+ content=TextContent(type="text", text=content),
149
+ role="assistant",
150
+ model=chat_completion.model,
151
+ )
152
+
153
+ raise ValueError("No content in response from completion")
154
+
155
+ def _select_model_from_preferences(
156
+ self, model_preferences: ModelPreferences | str | list[str] | None
157
+ ) -> ChatModel:
158
+ for model_option in self._iter_models_from_preferences(model_preferences):
159
+ if model_option in get_args(ChatModel):
160
+ chosen_model: ChatModel = model_option # pyright: ignore[reportAssignmentType]
161
+ return chosen_model
162
+
163
+ return self.default_model
@@ -110,10 +110,8 @@ def _determine_route_type(
110
110
  # Tags don't match, continue to next mapping
111
111
  continue
112
112
 
113
- # We know mcp_type is not None here due to post_init validation
114
- assert route_map.mcp_type is not None
115
113
  logger.debug(
116
- f"Route {route.method} {route.path} matched mapping to {route_map.mcp_type.name}"
114
+ f"Route {route.method} {route.path} mapped to {route_map.mcp_type.name}"
117
115
  )
118
116
  return route_map
119
117
 
@@ -11,7 +11,7 @@ from jsonschema_path import SchemaPath
11
11
  from fastmcp.experimental.utilities.openapi import (
12
12
  HTTPRoute,
13
13
  extract_output_schema_from_responses,
14
- format_description_with_responses,
14
+ format_simple_description,
15
15
  parse_openapi_to_http_routes,
16
16
  )
17
17
  from fastmcp.experimental.utilities.openapi.director import RequestDirector
@@ -151,9 +151,6 @@ class FastMCPOpenAPI(FastMCP):
151
151
  try:
152
152
  self._spec = SchemaPath.from_dict(openapi_spec) # type: ignore[arg-type]
153
153
  self._director = RequestDirector(self._spec)
154
- logger.debug(
155
- "Initialized OpenAPI RequestDirector for stateless request building"
156
- )
157
154
  except Exception as e:
158
155
  logger.error(f"Failed to initialize RequestDirector: {e}")
159
156
  raise ValueError(f"Invalid OpenAPI specification: {e}") from e
@@ -166,8 +163,6 @@ class FastMCPOpenAPI(FastMCP):
166
163
  # Determine route type based on mappings or default rules
167
164
  route_map = _determine_route_type(route, route_maps)
168
165
 
169
- # TODO: remove this once RouteType is removed and mcp_type is typed as MCPType without | None
170
- assert route_map.mcp_type is not None
171
166
  route_type = route_map.mcp_type
172
167
 
173
168
  # Call route_map_fn if provided
@@ -270,7 +265,9 @@ class FastMCPOpenAPI(FastMCP):
270
265
 
271
266
  # Extract output schema from OpenAPI responses
272
267
  output_schema = extract_output_schema_from_responses(
273
- route.responses, route.schema_definitions, route.openapi_version
268
+ route.responses,
269
+ route.response_schemas,
270
+ route.openapi_version,
274
271
  )
275
272
 
276
273
  # Get a unique tool name
@@ -282,10 +279,9 @@ class FastMCPOpenAPI(FastMCP):
282
279
  or f"Executes {route.method} {route.path}"
283
280
  )
284
281
 
285
- # Format enhanced description with parameters and request body
286
- enhanced_description = format_description_with_responses(
282
+ # Use simplified description formatter for tools
283
+ enhanced_description = format_simple_description(
287
284
  base_description=base_description,
288
- responses=route.responses,
289
285
  parameters=route.parameters,
290
286
  request_body=route.request_body,
291
287
  )
@@ -318,9 +314,6 @@ class FastMCPOpenAPI(FastMCP):
318
314
 
319
315
  # Register the tool by directly assigning to the tools dictionary
320
316
  self._tool_manager._tools[final_tool_name] = tool
321
- logger.debug(
322
- f"Registered TOOL: {final_tool_name} ({route.method} {route.path}) with tags: {route.tags}"
323
- )
324
317
 
325
318
  def _create_openapi_resource(
326
319
  self,
@@ -337,10 +330,9 @@ class FastMCPOpenAPI(FastMCP):
337
330
  route.description or route.summary or f"Represents {route.path}"
338
331
  )
339
332
 
340
- # Format enhanced description with parameters and request body
341
- enhanced_description = format_description_with_responses(
333
+ # Use simplified description for resources
334
+ enhanced_description = format_simple_description(
342
335
  base_description=base_description,
343
- responses=route.responses,
344
336
  parameters=route.parameters,
345
337
  request_body=route.request_body,
346
338
  )
@@ -372,9 +364,6 @@ class FastMCPOpenAPI(FastMCP):
372
364
 
373
365
  # Register the resource by directly assigning to the resources dictionary
374
366
  self._resource_manager._resources[final_resource_uri] = resource
375
- logger.debug(
376
- f"Registered RESOURCE: {final_resource_uri} ({route.method} {route.path}) with tags: {route.tags}"
377
- )
378
367
 
379
368
  def _create_openapi_template(
380
369
  self,
@@ -397,10 +386,9 @@ class FastMCPOpenAPI(FastMCP):
397
386
  route.description or route.summary or f"Template for {route.path}"
398
387
  )
399
388
 
400
- # Format enhanced description with parameters and request body
401
- enhanced_description = format_description_with_responses(
389
+ # Use simplified description for resource templates
390
+ enhanced_description = format_simple_description(
402
391
  base_description=base_description,
403
- responses=route.responses,
404
392
  parameters=route.parameters,
405
393
  request_body=route.request_body,
406
394
  )
@@ -455,9 +443,6 @@ class FastMCPOpenAPI(FastMCP):
455
443
 
456
444
  # Register the template by directly assigning to the templates dictionary
457
445
  self._resource_manager._templates[final_template_uri] = template
458
- logger.debug(
459
- f"Registered TEMPLATE: {final_template_uri} ({route.method} {route.path}) with tags: {route.tags}"
460
- )
461
446
 
462
447
 
463
448
  # Export public symbols