fastmcp 2.14.4__py3-none-any.whl → 3.0.0b1__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 (175) hide show
  1. fastmcp/_vendor/__init__.py +1 -0
  2. fastmcp/_vendor/docket_di/README.md +7 -0
  3. fastmcp/_vendor/docket_di/__init__.py +163 -0
  4. fastmcp/cli/cli.py +112 -28
  5. fastmcp/cli/install/claude_code.py +1 -5
  6. fastmcp/cli/install/claude_desktop.py +1 -5
  7. fastmcp/cli/install/cursor.py +1 -5
  8. fastmcp/cli/install/gemini_cli.py +1 -5
  9. fastmcp/cli/install/mcp_json.py +1 -6
  10. fastmcp/cli/run.py +146 -5
  11. fastmcp/client/__init__.py +7 -9
  12. fastmcp/client/auth/oauth.py +18 -17
  13. fastmcp/client/client.py +100 -870
  14. fastmcp/client/elicitation.py +1 -1
  15. fastmcp/client/mixins/__init__.py +13 -0
  16. fastmcp/client/mixins/prompts.py +295 -0
  17. fastmcp/client/mixins/resources.py +325 -0
  18. fastmcp/client/mixins/task_management.py +157 -0
  19. fastmcp/client/mixins/tools.py +397 -0
  20. fastmcp/client/sampling/handlers/anthropic.py +2 -2
  21. fastmcp/client/sampling/handlers/openai.py +1 -1
  22. fastmcp/client/tasks.py +3 -3
  23. fastmcp/client/telemetry.py +47 -0
  24. fastmcp/client/transports/__init__.py +38 -0
  25. fastmcp/client/transports/base.py +82 -0
  26. fastmcp/client/transports/config.py +170 -0
  27. fastmcp/client/transports/http.py +145 -0
  28. fastmcp/client/transports/inference.py +154 -0
  29. fastmcp/client/transports/memory.py +90 -0
  30. fastmcp/client/transports/sse.py +89 -0
  31. fastmcp/client/transports/stdio.py +543 -0
  32. fastmcp/contrib/component_manager/README.md +4 -10
  33. fastmcp/contrib/component_manager/__init__.py +1 -2
  34. fastmcp/contrib/component_manager/component_manager.py +95 -160
  35. fastmcp/contrib/component_manager/example.py +1 -1
  36. fastmcp/contrib/mcp_mixin/example.py +4 -4
  37. fastmcp/contrib/mcp_mixin/mcp_mixin.py +11 -4
  38. fastmcp/decorators.py +41 -0
  39. fastmcp/dependencies.py +12 -1
  40. fastmcp/exceptions.py +4 -0
  41. fastmcp/experimental/server/openapi/__init__.py +18 -15
  42. fastmcp/mcp_config.py +13 -4
  43. fastmcp/prompts/__init__.py +6 -3
  44. fastmcp/prompts/function_prompt.py +465 -0
  45. fastmcp/prompts/prompt.py +321 -271
  46. fastmcp/resources/__init__.py +5 -3
  47. fastmcp/resources/function_resource.py +335 -0
  48. fastmcp/resources/resource.py +325 -115
  49. fastmcp/resources/template.py +215 -43
  50. fastmcp/resources/types.py +27 -12
  51. fastmcp/server/__init__.py +2 -2
  52. fastmcp/server/auth/__init__.py +14 -0
  53. fastmcp/server/auth/auth.py +30 -10
  54. fastmcp/server/auth/authorization.py +190 -0
  55. fastmcp/server/auth/oauth_proxy/__init__.py +14 -0
  56. fastmcp/server/auth/oauth_proxy/consent.py +361 -0
  57. fastmcp/server/auth/oauth_proxy/models.py +178 -0
  58. fastmcp/server/auth/{oauth_proxy.py → oauth_proxy/proxy.py} +24 -778
  59. fastmcp/server/auth/oauth_proxy/ui.py +277 -0
  60. fastmcp/server/auth/oidc_proxy.py +2 -2
  61. fastmcp/server/auth/providers/auth0.py +24 -94
  62. fastmcp/server/auth/providers/aws.py +26 -95
  63. fastmcp/server/auth/providers/azure.py +41 -129
  64. fastmcp/server/auth/providers/descope.py +18 -49
  65. fastmcp/server/auth/providers/discord.py +25 -86
  66. fastmcp/server/auth/providers/github.py +23 -87
  67. fastmcp/server/auth/providers/google.py +24 -87
  68. fastmcp/server/auth/providers/introspection.py +60 -79
  69. fastmcp/server/auth/providers/jwt.py +30 -67
  70. fastmcp/server/auth/providers/oci.py +47 -110
  71. fastmcp/server/auth/providers/scalekit.py +23 -61
  72. fastmcp/server/auth/providers/supabase.py +18 -47
  73. fastmcp/server/auth/providers/workos.py +34 -127
  74. fastmcp/server/context.py +372 -419
  75. fastmcp/server/dependencies.py +541 -251
  76. fastmcp/server/elicitation.py +20 -18
  77. fastmcp/server/event_store.py +3 -3
  78. fastmcp/server/http.py +16 -6
  79. fastmcp/server/lifespan.py +198 -0
  80. fastmcp/server/low_level.py +92 -2
  81. fastmcp/server/middleware/__init__.py +5 -1
  82. fastmcp/server/middleware/authorization.py +312 -0
  83. fastmcp/server/middleware/caching.py +101 -54
  84. fastmcp/server/middleware/middleware.py +6 -9
  85. fastmcp/server/middleware/ping.py +70 -0
  86. fastmcp/server/middleware/tool_injection.py +2 -2
  87. fastmcp/server/mixins/__init__.py +7 -0
  88. fastmcp/server/mixins/lifespan.py +217 -0
  89. fastmcp/server/mixins/mcp_operations.py +392 -0
  90. fastmcp/server/mixins/transport.py +342 -0
  91. fastmcp/server/openapi/__init__.py +41 -21
  92. fastmcp/server/openapi/components.py +16 -339
  93. fastmcp/server/openapi/routing.py +34 -118
  94. fastmcp/server/openapi/server.py +67 -392
  95. fastmcp/server/providers/__init__.py +71 -0
  96. fastmcp/server/providers/aggregate.py +261 -0
  97. fastmcp/server/providers/base.py +578 -0
  98. fastmcp/server/providers/fastmcp_provider.py +674 -0
  99. fastmcp/server/providers/filesystem.py +226 -0
  100. fastmcp/server/providers/filesystem_discovery.py +327 -0
  101. fastmcp/server/providers/local_provider/__init__.py +11 -0
  102. fastmcp/server/providers/local_provider/decorators/__init__.py +15 -0
  103. fastmcp/server/providers/local_provider/decorators/prompts.py +256 -0
  104. fastmcp/server/providers/local_provider/decorators/resources.py +240 -0
  105. fastmcp/server/providers/local_provider/decorators/tools.py +315 -0
  106. fastmcp/server/providers/local_provider/local_provider.py +465 -0
  107. fastmcp/server/providers/openapi/__init__.py +39 -0
  108. fastmcp/server/providers/openapi/components.py +332 -0
  109. fastmcp/server/providers/openapi/provider.py +405 -0
  110. fastmcp/server/providers/openapi/routing.py +109 -0
  111. fastmcp/server/providers/proxy.py +867 -0
  112. fastmcp/server/providers/skills/__init__.py +59 -0
  113. fastmcp/server/providers/skills/_common.py +101 -0
  114. fastmcp/server/providers/skills/claude_provider.py +44 -0
  115. fastmcp/server/providers/skills/directory_provider.py +153 -0
  116. fastmcp/server/providers/skills/skill_provider.py +432 -0
  117. fastmcp/server/providers/skills/vendor_providers.py +142 -0
  118. fastmcp/server/providers/wrapped_provider.py +140 -0
  119. fastmcp/server/proxy.py +34 -700
  120. fastmcp/server/sampling/run.py +341 -2
  121. fastmcp/server/sampling/sampling_tool.py +4 -3
  122. fastmcp/server/server.py +1214 -2171
  123. fastmcp/server/tasks/__init__.py +2 -1
  124. fastmcp/server/tasks/capabilities.py +13 -1
  125. fastmcp/server/tasks/config.py +66 -3
  126. fastmcp/server/tasks/handlers.py +65 -273
  127. fastmcp/server/tasks/keys.py +4 -6
  128. fastmcp/server/tasks/requests.py +474 -0
  129. fastmcp/server/tasks/routing.py +76 -0
  130. fastmcp/server/tasks/subscriptions.py +20 -11
  131. fastmcp/server/telemetry.py +131 -0
  132. fastmcp/server/transforms/__init__.py +244 -0
  133. fastmcp/server/transforms/namespace.py +193 -0
  134. fastmcp/server/transforms/prompts_as_tools.py +175 -0
  135. fastmcp/server/transforms/resources_as_tools.py +190 -0
  136. fastmcp/server/transforms/tool_transform.py +96 -0
  137. fastmcp/server/transforms/version_filter.py +124 -0
  138. fastmcp/server/transforms/visibility.py +526 -0
  139. fastmcp/settings.py +34 -96
  140. fastmcp/telemetry.py +122 -0
  141. fastmcp/tools/__init__.py +10 -3
  142. fastmcp/tools/function_parsing.py +201 -0
  143. fastmcp/tools/function_tool.py +467 -0
  144. fastmcp/tools/tool.py +215 -362
  145. fastmcp/tools/tool_transform.py +38 -21
  146. fastmcp/utilities/async_utils.py +69 -0
  147. fastmcp/utilities/components.py +152 -91
  148. fastmcp/utilities/inspect.py +8 -20
  149. fastmcp/utilities/json_schema.py +12 -5
  150. fastmcp/utilities/json_schema_type.py +17 -15
  151. fastmcp/utilities/lifespan.py +56 -0
  152. fastmcp/utilities/logging.py +12 -4
  153. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
  154. fastmcp/utilities/openapi/parser.py +3 -3
  155. fastmcp/utilities/pagination.py +80 -0
  156. fastmcp/utilities/skills.py +253 -0
  157. fastmcp/utilities/tests.py +0 -16
  158. fastmcp/utilities/timeout.py +47 -0
  159. fastmcp/utilities/types.py +1 -1
  160. fastmcp/utilities/versions.py +285 -0
  161. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/METADATA +8 -5
  162. fastmcp-3.0.0b1.dist-info/RECORD +228 -0
  163. fastmcp/client/transports.py +0 -1170
  164. fastmcp/contrib/component_manager/component_service.py +0 -209
  165. fastmcp/prompts/prompt_manager.py +0 -117
  166. fastmcp/resources/resource_manager.py +0 -338
  167. fastmcp/server/tasks/converters.py +0 -206
  168. fastmcp/server/tasks/protocol.py +0 -359
  169. fastmcp/tools/tool_manager.py +0 -170
  170. fastmcp/utilities/mcp_config.py +0 -56
  171. fastmcp-2.14.4.dist-info/RECORD +0 -161
  172. /fastmcp/server/{openapi → providers/openapi}/README.md +0 -0
  173. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/WHEEL +0 -0
  174. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/entry_points.txt +0 -0
  175. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -3,8 +3,10 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import inspect
6
+ import json
7
+ from collections.abc import Callable, Sequence
6
8
  from dataclasses import dataclass
7
- from typing import TYPE_CHECKING, Generic
9
+ from typing import TYPE_CHECKING, Any, Generic, Literal, cast
8
10
 
9
11
  from mcp.types import (
10
12
  ClientCapabilities,
@@ -14,6 +16,7 @@ from mcp.types import (
14
16
  ModelPreferences,
15
17
  SamplingCapability,
16
18
  SamplingMessage,
19
+ SamplingMessageContentBlock,
17
20
  SamplingToolsCapability,
18
21
  TextContent,
19
22
  ToolChoice,
@@ -22,18 +25,25 @@ from mcp.types import (
22
25
  )
23
26
  from mcp.types import CreateMessageRequestParams as SamplingParams
24
27
  from mcp.types import Tool as SDKTool
28
+ from pydantic import ValidationError
25
29
  from typing_extensions import TypeVar
26
30
 
31
+ from fastmcp import settings
27
32
  from fastmcp.exceptions import ToolError
28
33
  from fastmcp.server.sampling.sampling_tool import SamplingTool
34
+ from fastmcp.utilities.json_schema import compress_schema
29
35
  from fastmcp.utilities.logging import get_logger
36
+ from fastmcp.utilities.types import get_cached_typeadapter
30
37
 
31
38
  logger = get_logger(__name__)
32
39
 
33
40
  if TYPE_CHECKING:
34
41
  from fastmcp.server.context import Context
35
42
 
36
- ResultT = TypeVar("ResultT", default=str)
43
+ ResultT = TypeVar("ResultT")
44
+
45
+ # Simplified tool choice type - just the mode string instead of the full MCP object
46
+ ToolChoiceOption = Literal["auto", "required", "none"]
37
47
 
38
48
 
39
49
  @dataclass
@@ -299,3 +309,332 @@ async def execute_tools(
299
309
  )
300
310
 
301
311
  return tool_results
312
+
313
+
314
+ # --- Helper functions for sampling ---
315
+
316
+
317
+ def prepare_messages(
318
+ messages: str | Sequence[str | SamplingMessage],
319
+ ) -> list[SamplingMessage]:
320
+ """Convert various message formats to a list of SamplingMessage objects."""
321
+ if isinstance(messages, str):
322
+ return [
323
+ SamplingMessage(
324
+ content=TextContent(text=messages, type="text"), role="user"
325
+ )
326
+ ]
327
+ else:
328
+ return [
329
+ SamplingMessage(content=TextContent(text=m, type="text"), role="user")
330
+ if isinstance(m, str)
331
+ else m
332
+ for m in messages
333
+ ]
334
+
335
+
336
+ def prepare_tools(
337
+ tools: Sequence[SamplingTool | Callable[..., Any]] | None,
338
+ ) -> list[SamplingTool] | None:
339
+ """Convert tools to SamplingTool objects."""
340
+ if tools is None:
341
+ return None
342
+
343
+ sampling_tools: list[SamplingTool] = []
344
+ for t in tools:
345
+ if isinstance(t, SamplingTool):
346
+ sampling_tools.append(t)
347
+ elif callable(t):
348
+ sampling_tools.append(SamplingTool.from_function(t))
349
+ else:
350
+ raise TypeError(f"Expected SamplingTool or callable, got {type(t)}")
351
+
352
+ return sampling_tools if sampling_tools else None
353
+
354
+
355
+ def extract_tool_calls(
356
+ response: CreateMessageResult | CreateMessageResultWithTools,
357
+ ) -> list[ToolUseContent]:
358
+ """Extract tool calls from a response."""
359
+ content = response.content
360
+ if isinstance(content, list):
361
+ return [c for c in content if isinstance(c, ToolUseContent)]
362
+ elif isinstance(content, ToolUseContent):
363
+ return [content]
364
+ return []
365
+
366
+
367
+ def create_final_response_tool(result_type: type) -> SamplingTool:
368
+ """Create a synthetic 'final_response' tool for structured output.
369
+
370
+ This tool is used to capture structured responses from the LLM.
371
+ The tool's schema is derived from the result_type.
372
+ """
373
+ type_adapter = get_cached_typeadapter(result_type)
374
+ schema = type_adapter.json_schema()
375
+ schema = compress_schema(schema, prune_titles=True)
376
+
377
+ # Tool parameters must be object-shaped. Wrap primitives in {"value": <schema>}
378
+ if schema.get("type") != "object":
379
+ schema = {
380
+ "type": "object",
381
+ "properties": {"value": schema},
382
+ "required": ["value"],
383
+ }
384
+
385
+ # The fn just returns the input as-is (validation happens in the loop)
386
+ def final_response(**kwargs: Any) -> dict[str, Any]:
387
+ return kwargs
388
+
389
+ return SamplingTool(
390
+ name="final_response",
391
+ description=(
392
+ "Call this tool to provide your final response. "
393
+ "Use this when you have completed the task and are ready to return the result."
394
+ ),
395
+ parameters=schema,
396
+ fn=final_response,
397
+ )
398
+
399
+
400
+ # --- Implementation functions for Context methods ---
401
+
402
+
403
+ async def sample_step_impl(
404
+ context: Context,
405
+ messages: str | Sequence[str | SamplingMessage],
406
+ *,
407
+ system_prompt: str | None = None,
408
+ temperature: float | None = None,
409
+ max_tokens: int | None = None,
410
+ model_preferences: ModelPreferences | str | list[str] | None = None,
411
+ tools: Sequence[SamplingTool | Callable[..., Any]] | None = None,
412
+ tool_choice: ToolChoiceOption | str | None = None,
413
+ auto_execute_tools: bool = True,
414
+ mask_error_details: bool | None = None,
415
+ ) -> SampleStep:
416
+ """Implementation of Context.sample_step().
417
+
418
+ Make a single LLM sampling call. This is a stateless function that makes
419
+ exactly one LLM call and optionally executes any requested tools.
420
+ """
421
+ # Convert messages to SamplingMessage objects
422
+ current_messages = prepare_messages(messages)
423
+
424
+ # Convert tools to SamplingTools
425
+ sampling_tools = prepare_tools(tools)
426
+ sdk_tools: list[SDKTool] | None = (
427
+ [t._to_sdk_tool() for t in sampling_tools] if sampling_tools else None
428
+ )
429
+ tool_map: dict[str, SamplingTool] = (
430
+ {t.name: t for t in sampling_tools} if sampling_tools else {}
431
+ )
432
+
433
+ # Determine whether to use fallback handler or client
434
+ use_fallback = determine_handler_mode(context, bool(sampling_tools))
435
+
436
+ # Build tool choice
437
+ effective_tool_choice: ToolChoice | None = None
438
+ if tool_choice is not None:
439
+ if tool_choice not in ("auto", "required", "none"):
440
+ raise ValueError(
441
+ f"Invalid tool_choice: {tool_choice!r}. "
442
+ "Must be 'auto', 'required', or 'none'."
443
+ )
444
+ effective_tool_choice = ToolChoice(
445
+ mode=cast(Literal["auto", "required", "none"], tool_choice)
446
+ )
447
+
448
+ # Effective max_tokens
449
+ effective_max_tokens = max_tokens if max_tokens is not None else 512
450
+
451
+ # Make the LLM call
452
+ if use_fallback:
453
+ response = await call_sampling_handler(
454
+ context,
455
+ current_messages,
456
+ system_prompt=system_prompt,
457
+ temperature=temperature,
458
+ max_tokens=effective_max_tokens,
459
+ model_preferences=model_preferences,
460
+ sdk_tools=sdk_tools,
461
+ tool_choice=effective_tool_choice,
462
+ )
463
+ else:
464
+ response = await context.session.create_message(
465
+ messages=current_messages,
466
+ system_prompt=system_prompt,
467
+ temperature=temperature,
468
+ max_tokens=effective_max_tokens,
469
+ model_preferences=_parse_model_preferences(model_preferences),
470
+ tools=sdk_tools,
471
+ tool_choice=effective_tool_choice,
472
+ related_request_id=context.request_id,
473
+ )
474
+
475
+ # Check if this is a tool use response
476
+ is_tool_use_response = (
477
+ isinstance(response, CreateMessageResultWithTools)
478
+ and response.stopReason == "toolUse"
479
+ )
480
+
481
+ # Always include the assistant response in history
482
+ current_messages.append(SamplingMessage(role="assistant", content=response.content))
483
+
484
+ # If not a tool use, return immediately
485
+ if not is_tool_use_response:
486
+ return SampleStep(response=response, history=current_messages)
487
+
488
+ # If not executing tools, return with assistant message but no tool results
489
+ if not auto_execute_tools:
490
+ return SampleStep(response=response, history=current_messages)
491
+
492
+ # Execute tools and add results to history
493
+ step_tool_calls = extract_tool_calls(response)
494
+ if step_tool_calls:
495
+ effective_mask = (
496
+ mask_error_details
497
+ if mask_error_details is not None
498
+ else settings.mask_error_details
499
+ )
500
+ tool_results: list[ToolResultContent] = await execute_tools(
501
+ step_tool_calls, tool_map, mask_error_details=effective_mask
502
+ )
503
+
504
+ if tool_results:
505
+ current_messages.append(
506
+ SamplingMessage(
507
+ role="user",
508
+ content=cast(list[SamplingMessageContentBlock], tool_results),
509
+ )
510
+ )
511
+
512
+ return SampleStep(response=response, history=current_messages)
513
+
514
+
515
+ async def sample_impl(
516
+ context: Context,
517
+ messages: str | Sequence[str | SamplingMessage],
518
+ *,
519
+ system_prompt: str | None = None,
520
+ temperature: float | None = None,
521
+ max_tokens: int | None = None,
522
+ model_preferences: ModelPreferences | str | list[str] | None = None,
523
+ tools: Sequence[SamplingTool | Callable[..., Any]] | None = None,
524
+ result_type: type[ResultT] | None = None,
525
+ mask_error_details: bool | None = None,
526
+ ) -> SamplingResult[ResultT]:
527
+ """Implementation of Context.sample().
528
+
529
+ Send a sampling request to the client and await the response. This method
530
+ runs to completion automatically, executing a tool loop until the LLM
531
+ provides a final text response.
532
+ """
533
+ # Safety limit to prevent infinite loops
534
+ max_iterations = 100
535
+
536
+ # Convert tools to SamplingTools
537
+ sampling_tools = prepare_tools(tools)
538
+
539
+ # Handle structured output with result_type
540
+ tool_choice: str | None = None
541
+ if result_type is not None and result_type is not str:
542
+ final_response_tool = create_final_response_tool(result_type)
543
+ sampling_tools = list(sampling_tools) if sampling_tools else []
544
+ sampling_tools.append(final_response_tool)
545
+
546
+ # Always require tool calls when result_type is set - the LLM must
547
+ # eventually call final_response (text responses are not accepted)
548
+ tool_choice = "required"
549
+
550
+ # Convert messages for the loop
551
+ current_messages: str | Sequence[str | SamplingMessage] = messages
552
+
553
+ for _iteration in range(max_iterations):
554
+ step = await sample_step_impl(
555
+ context,
556
+ messages=current_messages,
557
+ system_prompt=system_prompt,
558
+ temperature=temperature,
559
+ max_tokens=max_tokens,
560
+ model_preferences=model_preferences,
561
+ tools=sampling_tools,
562
+ tool_choice=tool_choice,
563
+ mask_error_details=mask_error_details,
564
+ )
565
+
566
+ # Check for final_response tool call for structured output
567
+ if result_type is not None and result_type is not str and step.is_tool_use:
568
+ for tool_call in step.tool_calls:
569
+ if tool_call.name == "final_response":
570
+ # Validate and return the structured result
571
+ type_adapter = get_cached_typeadapter(result_type)
572
+
573
+ # Unwrap if we wrapped primitives (non-object schemas)
574
+ input_data = tool_call.input
575
+ original_schema = compress_schema(
576
+ type_adapter.json_schema(), prune_titles=True
577
+ )
578
+ if (
579
+ original_schema.get("type") != "object"
580
+ and isinstance(input_data, dict)
581
+ and "value" in input_data
582
+ ):
583
+ input_data = input_data["value"]
584
+
585
+ try:
586
+ validated_result = type_adapter.validate_python(input_data)
587
+ text = json.dumps(
588
+ type_adapter.dump_python(validated_result, mode="json")
589
+ )
590
+ return SamplingResult(
591
+ text=text,
592
+ result=validated_result,
593
+ history=step.history,
594
+ )
595
+ except ValidationError as e:
596
+ # Validation failed - add error as tool result
597
+ step.history.append(
598
+ SamplingMessage(
599
+ role="user",
600
+ content=[
601
+ ToolResultContent(
602
+ type="tool_result",
603
+ toolUseId=tool_call.id,
604
+ content=[
605
+ TextContent(
606
+ type="text",
607
+ text=(
608
+ f"Validation error: {e}. "
609
+ "Please try again with valid data."
610
+ ),
611
+ )
612
+ ],
613
+ isError=True,
614
+ )
615
+ ],
616
+ )
617
+ )
618
+
619
+ # If not a tool use response, we're done
620
+ if not step.is_tool_use:
621
+ # For structured output, the LLM must use the final_response tool
622
+ if result_type is not None and result_type is not str:
623
+ raise RuntimeError(
624
+ f"Expected structured output of type {result_type.__name__}, "
625
+ "but the LLM returned a text response instead of calling "
626
+ "the final_response tool."
627
+ )
628
+ return SamplingResult(
629
+ text=step.text,
630
+ result=cast(ResultT, step.text if step.text else ""),
631
+ history=step.history,
632
+ )
633
+
634
+ # Continue with the updated history
635
+ current_messages = step.history
636
+
637
+ # After first iteration, reset tool_choice to auto
638
+ tool_choice = None
639
+
640
+ raise RuntimeError(f"Sampling exceeded maximum iterations ({max_iterations})")
@@ -7,12 +7,13 @@ from collections.abc import Callable
7
7
  from typing import Any
8
8
 
9
9
  from mcp.types import Tool as SDKTool
10
- from pydantic import BaseModel, ConfigDict
10
+ from pydantic import ConfigDict
11
11
 
12
- from fastmcp.tools.tool import ParsedFunction
12
+ from fastmcp.tools.function_parsing import ParsedFunction
13
+ from fastmcp.utilities.types import FastMCPBaseModel
13
14
 
14
15
 
15
- class SamplingTool(BaseModel):
16
+ class SamplingTool(FastMCPBaseModel):
16
17
  """A tool that can be used during LLM sampling.
17
18
 
18
19
  SamplingTools bundle a tool's schema (name, description, parameters) with