fastmcp 2.13.3__py3-none-any.whl → 2.14.1__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 (85) hide show
  1. fastmcp/__init__.py +0 -21
  2. fastmcp/cli/__init__.py +0 -3
  3. fastmcp/cli/__main__.py +5 -0
  4. fastmcp/cli/cli.py +8 -22
  5. fastmcp/cli/install/shared.py +0 -15
  6. fastmcp/cli/tasks.py +110 -0
  7. fastmcp/client/auth/oauth.py +9 -9
  8. fastmcp/client/client.py +739 -136
  9. fastmcp/client/elicitation.py +11 -5
  10. fastmcp/client/messages.py +7 -5
  11. fastmcp/client/roots.py +2 -1
  12. fastmcp/client/sampling/__init__.py +69 -0
  13. fastmcp/client/sampling/handlers/__init__.py +0 -0
  14. fastmcp/client/sampling/handlers/anthropic.py +387 -0
  15. fastmcp/client/sampling/handlers/openai.py +399 -0
  16. fastmcp/client/tasks.py +551 -0
  17. fastmcp/client/transports.py +72 -21
  18. fastmcp/contrib/component_manager/component_service.py +4 -20
  19. fastmcp/dependencies.py +25 -0
  20. fastmcp/experimental/sampling/handlers/__init__.py +5 -0
  21. fastmcp/experimental/sampling/handlers/openai.py +4 -169
  22. fastmcp/experimental/server/openapi/__init__.py +15 -13
  23. fastmcp/experimental/utilities/openapi/__init__.py +12 -38
  24. fastmcp/prompts/prompt.py +38 -38
  25. fastmcp/resources/resource.py +33 -16
  26. fastmcp/resources/template.py +69 -59
  27. fastmcp/server/auth/__init__.py +0 -9
  28. fastmcp/server/auth/auth.py +127 -3
  29. fastmcp/server/auth/oauth_proxy.py +47 -97
  30. fastmcp/server/auth/oidc_proxy.py +7 -0
  31. fastmcp/server/auth/providers/in_memory.py +2 -2
  32. fastmcp/server/auth/providers/oci.py +2 -2
  33. fastmcp/server/context.py +509 -180
  34. fastmcp/server/dependencies.py +464 -6
  35. fastmcp/server/elicitation.py +285 -47
  36. fastmcp/server/event_store.py +177 -0
  37. fastmcp/server/http.py +15 -3
  38. fastmcp/server/low_level.py +56 -12
  39. fastmcp/server/middleware/middleware.py +2 -2
  40. fastmcp/server/openapi/__init__.py +35 -0
  41. fastmcp/{experimental/server → server}/openapi/components.py +4 -3
  42. fastmcp/{experimental/server → server}/openapi/routing.py +1 -1
  43. fastmcp/{experimental/server → server}/openapi/server.py +6 -5
  44. fastmcp/server/proxy.py +53 -40
  45. fastmcp/server/sampling/__init__.py +10 -0
  46. fastmcp/server/sampling/run.py +301 -0
  47. fastmcp/server/sampling/sampling_tool.py +108 -0
  48. fastmcp/server/server.py +793 -552
  49. fastmcp/server/tasks/__init__.py +21 -0
  50. fastmcp/server/tasks/capabilities.py +22 -0
  51. fastmcp/server/tasks/config.py +89 -0
  52. fastmcp/server/tasks/converters.py +206 -0
  53. fastmcp/server/tasks/handlers.py +356 -0
  54. fastmcp/server/tasks/keys.py +93 -0
  55. fastmcp/server/tasks/protocol.py +355 -0
  56. fastmcp/server/tasks/subscriptions.py +205 -0
  57. fastmcp/settings.py +101 -103
  58. fastmcp/tools/tool.py +83 -49
  59. fastmcp/tools/tool_transform.py +1 -12
  60. fastmcp/utilities/components.py +3 -3
  61. fastmcp/utilities/json_schema_type.py +4 -4
  62. fastmcp/utilities/mcp_config.py +1 -2
  63. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +1 -1
  64. fastmcp/{experimental/utilities → utilities}/openapi/README.md +7 -35
  65. fastmcp/utilities/openapi/__init__.py +63 -0
  66. fastmcp/{experimental/utilities → utilities}/openapi/formatters.py +5 -5
  67. fastmcp/{experimental/utilities → utilities}/openapi/json_schema_converter.py +1 -1
  68. fastmcp/utilities/tests.py +11 -5
  69. fastmcp/utilities/types.py +8 -0
  70. {fastmcp-2.13.3.dist-info → fastmcp-2.14.1.dist-info}/METADATA +7 -4
  71. {fastmcp-2.13.3.dist-info → fastmcp-2.14.1.dist-info}/RECORD +79 -63
  72. fastmcp/client/sampling.py +0 -56
  73. fastmcp/experimental/sampling/handlers/base.py +0 -21
  74. fastmcp/server/auth/providers/bearer.py +0 -25
  75. fastmcp/server/openapi.py +0 -1087
  76. fastmcp/server/sampling/handler.py +0 -19
  77. fastmcp/utilities/openapi.py +0 -1568
  78. /fastmcp/{experimental/server → server}/openapi/README.md +0 -0
  79. /fastmcp/{experimental/utilities → utilities}/openapi/director.py +0 -0
  80. /fastmcp/{experimental/utilities → utilities}/openapi/models.py +0 -0
  81. /fastmcp/{experimental/utilities → utilities}/openapi/parser.py +0 -0
  82. /fastmcp/{experimental/utilities → utilities}/openapi/schemas.py +0 -0
  83. {fastmcp-2.13.3.dist-info → fastmcp-2.14.1.dist-info}/WHEEL +0 -0
  84. {fastmcp-2.13.3.dist-info → fastmcp-2.14.1.dist-info}/entry_points.txt +0 -0
  85. {fastmcp-2.13.3.dist-info → fastmcp-2.14.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,301 @@
1
+ """Sampling types and helper functions for FastMCP servers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from dataclasses import dataclass
7
+ from typing import TYPE_CHECKING, Generic
8
+
9
+ from mcp.types import (
10
+ ClientCapabilities,
11
+ CreateMessageResult,
12
+ CreateMessageResultWithTools,
13
+ ModelHint,
14
+ ModelPreferences,
15
+ SamplingCapability,
16
+ SamplingMessage,
17
+ SamplingToolsCapability,
18
+ TextContent,
19
+ ToolChoice,
20
+ ToolResultContent,
21
+ ToolUseContent,
22
+ )
23
+ from mcp.types import CreateMessageRequestParams as SamplingParams
24
+ from mcp.types import Tool as SDKTool
25
+ from typing_extensions import TypeVar
26
+
27
+ from fastmcp.exceptions import ToolError
28
+ from fastmcp.server.sampling.sampling_tool import SamplingTool
29
+ from fastmcp.utilities.logging import get_logger
30
+
31
+ logger = get_logger(__name__)
32
+
33
+ if TYPE_CHECKING:
34
+ from fastmcp.server.context import Context
35
+
36
+ ResultT = TypeVar("ResultT", default=str)
37
+
38
+
39
+ @dataclass
40
+ class SamplingResult(Generic[ResultT]):
41
+ """Result of a sampling operation.
42
+
43
+ Attributes:
44
+ text: The text representation of the result (raw text or JSON for structured).
45
+ result: The typed result (str for text, parsed object for structured output).
46
+ history: All messages exchanged during sampling.
47
+ """
48
+
49
+ text: str | None
50
+ result: ResultT
51
+ history: list[SamplingMessage]
52
+
53
+
54
+ @dataclass
55
+ class SampleStep:
56
+ """Result of a single sampling call.
57
+
58
+ Represents what the LLM returned in this step plus the message history.
59
+ """
60
+
61
+ response: CreateMessageResult | CreateMessageResultWithTools
62
+ history: list[SamplingMessage]
63
+
64
+ @property
65
+ def is_tool_use(self) -> bool:
66
+ """True if the LLM is requesting tool execution."""
67
+ if isinstance(self.response, CreateMessageResultWithTools):
68
+ return self.response.stopReason == "toolUse"
69
+ return False
70
+
71
+ @property
72
+ def text(self) -> str | None:
73
+ """Extract text from the response, if available."""
74
+ content = self.response.content
75
+ if isinstance(content, list):
76
+ for block in content:
77
+ if isinstance(block, TextContent):
78
+ return block.text
79
+ return None
80
+ elif isinstance(content, TextContent):
81
+ return content.text
82
+ return None
83
+
84
+ @property
85
+ def tool_calls(self) -> list[ToolUseContent]:
86
+ """Get the list of tool calls from the response."""
87
+ content = self.response.content
88
+ if isinstance(content, list):
89
+ return [c for c in content if isinstance(c, ToolUseContent)]
90
+ elif isinstance(content, ToolUseContent):
91
+ return [content]
92
+ return []
93
+
94
+
95
+ def _parse_model_preferences(
96
+ model_preferences: ModelPreferences | str | list[str] | None,
97
+ ) -> ModelPreferences | None:
98
+ """Convert model preferences to ModelPreferences object."""
99
+ if model_preferences is None:
100
+ return None
101
+ elif isinstance(model_preferences, ModelPreferences):
102
+ return model_preferences
103
+ elif isinstance(model_preferences, str):
104
+ return ModelPreferences(hints=[ModelHint(name=model_preferences)])
105
+ elif isinstance(model_preferences, list):
106
+ if not all(isinstance(h, str) for h in model_preferences):
107
+ raise ValueError("All elements of model_preferences list must be strings.")
108
+ return ModelPreferences(hints=[ModelHint(name=h) for h in model_preferences])
109
+ else:
110
+ raise ValueError(
111
+ "model_preferences must be one of: ModelPreferences, str, list[str], or None."
112
+ )
113
+
114
+
115
+ # --- Standalone functions for sample_step() ---
116
+
117
+
118
+ def determine_handler_mode(context: Context, needs_tools: bool) -> bool:
119
+ """Determine whether to use fallback handler or client for sampling.
120
+
121
+ Args:
122
+ context: The MCP context.
123
+ needs_tools: Whether the sampling request requires tool support.
124
+
125
+ Returns:
126
+ True if fallback handler should be used, False to use client.
127
+
128
+ Raises:
129
+ ValueError: If client lacks required capability and no fallback configured.
130
+ """
131
+ fastmcp = context.fastmcp
132
+ session = context.session
133
+
134
+ # Check what capabilities the client has
135
+ has_sampling = session.check_client_capability(
136
+ capability=ClientCapabilities(sampling=SamplingCapability())
137
+ )
138
+ has_tools_capability = session.check_client_capability(
139
+ capability=ClientCapabilities(
140
+ sampling=SamplingCapability(tools=SamplingToolsCapability())
141
+ )
142
+ )
143
+
144
+ if fastmcp.sampling_handler_behavior == "always":
145
+ if fastmcp.sampling_handler is None:
146
+ raise ValueError(
147
+ "sampling_handler_behavior is 'always' but no handler configured"
148
+ )
149
+ return True
150
+ elif fastmcp.sampling_handler_behavior == "fallback":
151
+ client_sufficient = has_sampling and (not needs_tools or has_tools_capability)
152
+ if not client_sufficient:
153
+ if fastmcp.sampling_handler is None:
154
+ if needs_tools and has_sampling and not has_tools_capability:
155
+ raise ValueError(
156
+ "Client does not support sampling with tools. "
157
+ "The client must advertise the sampling.tools capability."
158
+ )
159
+ raise ValueError("Client does not support sampling")
160
+ return True
161
+ elif fastmcp.sampling_handler_behavior is not None:
162
+ raise ValueError(
163
+ f"Invalid sampling_handler_behavior: {fastmcp.sampling_handler_behavior!r}. "
164
+ "Must be 'always', 'fallback', or None."
165
+ )
166
+ elif not has_sampling:
167
+ raise ValueError("Client does not support sampling")
168
+ elif needs_tools and not has_tools_capability:
169
+ raise ValueError(
170
+ "Client does not support sampling with tools. "
171
+ "The client must advertise the sampling.tools capability."
172
+ )
173
+
174
+ return False
175
+
176
+
177
+ async def call_sampling_handler(
178
+ context: Context,
179
+ messages: list[SamplingMessage],
180
+ *,
181
+ system_prompt: str | None,
182
+ temperature: float | None,
183
+ max_tokens: int,
184
+ model_preferences: ModelPreferences | str | list[str] | None,
185
+ sdk_tools: list[SDKTool] | None,
186
+ tool_choice: ToolChoice | None,
187
+ ) -> CreateMessageResult | CreateMessageResultWithTools:
188
+ """Make LLM call using the fallback handler.
189
+
190
+ Note: This function expects the caller (sample_step) to have validated that
191
+ sampling_handler is set via determine_handler_mode(). The checks below are
192
+ safeguards against internal misuse.
193
+ """
194
+ if context.fastmcp.sampling_handler is None:
195
+ raise RuntimeError("sampling_handler is None")
196
+ if context.request_context is None:
197
+ raise RuntimeError("request_context is None")
198
+
199
+ result = context.fastmcp.sampling_handler(
200
+ messages,
201
+ SamplingParams(
202
+ systemPrompt=system_prompt,
203
+ messages=messages,
204
+ temperature=temperature,
205
+ maxTokens=max_tokens,
206
+ modelPreferences=_parse_model_preferences(model_preferences),
207
+ tools=sdk_tools,
208
+ toolChoice=tool_choice,
209
+ ),
210
+ context.request_context,
211
+ )
212
+
213
+ if inspect.isawaitable(result):
214
+ result = await result
215
+
216
+ # Convert string to CreateMessageResult
217
+ if isinstance(result, str):
218
+ return CreateMessageResult(
219
+ role="assistant",
220
+ content=TextContent(type="text", text=result),
221
+ model="unknown",
222
+ stopReason="endTurn",
223
+ )
224
+
225
+ return result
226
+
227
+
228
+ async def execute_tools(
229
+ tool_calls: list[ToolUseContent],
230
+ tool_map: dict[str, SamplingTool],
231
+ mask_error_details: bool = False,
232
+ ) -> list[ToolResultContent]:
233
+ """Execute tool calls and return results.
234
+
235
+ Args:
236
+ tool_calls: List of tool use requests from the LLM.
237
+ tool_map: Mapping from tool name to SamplingTool.
238
+ mask_error_details: If True, mask detailed error messages from tool execution.
239
+ When masked, only generic error messages are returned to the LLM.
240
+ Tools can explicitly raise ToolError to bypass masking when they want
241
+ to provide specific error messages to the LLM.
242
+
243
+ Returns:
244
+ List of tool result content blocks.
245
+ """
246
+ tool_results: list[ToolResultContent] = []
247
+
248
+ for tool_use in tool_calls:
249
+ tool = tool_map.get(tool_use.name)
250
+ if tool is None:
251
+ tool_results.append(
252
+ ToolResultContent(
253
+ type="tool_result",
254
+ toolUseId=tool_use.id,
255
+ content=[
256
+ TextContent(
257
+ type="text",
258
+ text=f"Error: Unknown tool '{tool_use.name}'",
259
+ )
260
+ ],
261
+ isError=True,
262
+ )
263
+ )
264
+ else:
265
+ try:
266
+ result_value = await tool.run(tool_use.input)
267
+ tool_results.append(
268
+ ToolResultContent(
269
+ type="tool_result",
270
+ toolUseId=tool_use.id,
271
+ content=[TextContent(type="text", text=str(result_value))],
272
+ )
273
+ )
274
+ except ToolError as e:
275
+ # ToolError is the escape hatch - always pass message through
276
+ logger.exception(f"Error calling sampling tool '{tool_use.name}'")
277
+ tool_results.append(
278
+ ToolResultContent(
279
+ type="tool_result",
280
+ toolUseId=tool_use.id,
281
+ content=[TextContent(type="text", text=str(e))],
282
+ isError=True,
283
+ )
284
+ )
285
+ except Exception as e:
286
+ # Generic exceptions - mask based on setting
287
+ logger.exception(f"Error calling sampling tool '{tool_use.name}'")
288
+ if mask_error_details:
289
+ error_text = f"Error executing tool '{tool_use.name}'"
290
+ else:
291
+ error_text = f"Error executing tool '{tool_use.name}': {e}"
292
+ tool_results.append(
293
+ ToolResultContent(
294
+ type="tool_result",
295
+ toolUseId=tool_use.id,
296
+ content=[TextContent(type="text", text=error_text)],
297
+ isError=True,
298
+ )
299
+ )
300
+
301
+ return tool_results
@@ -0,0 +1,108 @@
1
+ """SamplingTool for use during LLM sampling requests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from collections.abc import Callable
7
+ from typing import Any
8
+
9
+ from mcp.types import Tool as SDKTool
10
+ from pydantic import BaseModel, ConfigDict
11
+
12
+ from fastmcp.tools.tool import ParsedFunction
13
+
14
+
15
+ class SamplingTool(BaseModel):
16
+ """A tool that can be used during LLM sampling.
17
+
18
+ SamplingTools bundle a tool's schema (name, description, parameters) with
19
+ an executor function, enabling servers to execute agentic workflows where
20
+ the LLM can request tool calls during sampling.
21
+
22
+ In most cases, pass functions directly to ctx.sample():
23
+
24
+ def search(query: str) -> str:
25
+ '''Search the web.'''
26
+ return web_search(query)
27
+
28
+ result = await context.sample(
29
+ messages="Find info about Python",
30
+ tools=[search], # Plain functions work directly
31
+ )
32
+
33
+ Create a SamplingTool explicitly when you need custom name/description:
34
+
35
+ tool = SamplingTool.from_function(search, name="web_search")
36
+ """
37
+
38
+ name: str
39
+ description: str | None = None
40
+ parameters: dict[str, Any]
41
+ fn: Callable[..., Any]
42
+
43
+ model_config = ConfigDict(arbitrary_types_allowed=True)
44
+
45
+ async def run(self, arguments: dict[str, Any] | None = None) -> Any:
46
+ """Execute the tool with the given arguments.
47
+
48
+ Args:
49
+ arguments: Dictionary of arguments to pass to the tool function.
50
+
51
+ Returns:
52
+ The result of executing the tool function.
53
+ """
54
+ if arguments is None:
55
+ arguments = {}
56
+
57
+ result = self.fn(**arguments)
58
+ if inspect.isawaitable(result):
59
+ result = await result
60
+ return result
61
+
62
+ def _to_sdk_tool(self) -> SDKTool:
63
+ """Convert to an mcp.types.Tool for SDK compatibility.
64
+
65
+ This is used internally when passing tools to the MCP SDK's
66
+ create_message() method.
67
+ """
68
+ return SDKTool(
69
+ name=self.name,
70
+ description=self.description,
71
+ inputSchema=self.parameters,
72
+ )
73
+
74
+ @classmethod
75
+ def from_function(
76
+ cls,
77
+ fn: Callable[..., Any],
78
+ *,
79
+ name: str | None = None,
80
+ description: str | None = None,
81
+ ) -> SamplingTool:
82
+ """Create a SamplingTool from a function.
83
+
84
+ The function's signature is analyzed to generate a JSON schema for
85
+ the tool's parameters. Type hints are used to determine parameter types.
86
+
87
+ Args:
88
+ fn: The function to create a tool from.
89
+ name: Optional name override. Defaults to the function's name.
90
+ description: Optional description override. Defaults to the function's docstring.
91
+
92
+ Returns:
93
+ A SamplingTool wrapping the function.
94
+
95
+ Raises:
96
+ ValueError: If the function is a lambda without a name override.
97
+ """
98
+ parsed = ParsedFunction.from_function(fn, validate=True)
99
+
100
+ if name is None and parsed.name == "<lambda>":
101
+ raise ValueError("You must provide a name for lambda functions")
102
+
103
+ return cls(
104
+ name=name or parsed.name,
105
+ description=description or parsed.description,
106
+ parameters=parsed.input_schema,
107
+ fn=parsed.fn,
108
+ )