fastmcp 2.10.2__py3-none-any.whl → 2.10.4__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.
fastmcp/server/proxy.py CHANGED
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import warnings
4
+ from collections.abc import Callable
3
5
  from pathlib import Path
4
6
  from typing import TYPE_CHECKING, Any, cast
5
7
  from urllib.parse import quote
@@ -16,12 +18,14 @@ from mcp.types import (
16
18
  )
17
19
  from pydantic.networks import AnyUrl
18
20
 
19
- from fastmcp.client import Client
21
+ import fastmcp
22
+ from fastmcp.client.client import Client, FastMCP1Server
20
23
  from fastmcp.client.elicitation import ElicitResult
21
24
  from fastmcp.client.logging import LogMessage
22
25
  from fastmcp.client.roots import RootsList
23
26
  from fastmcp.client.transports import ClientTransportT
24
27
  from fastmcp.exceptions import NotFoundError, ResourceError, ToolError
28
+ from fastmcp.mcp_config import MCPConfig
25
29
  from fastmcp.prompts import Prompt, PromptMessage
26
30
  from fastmcp.prompts.prompt import PromptArgument
27
31
  from fastmcp.prompts.prompt_manager import PromptManager
@@ -33,7 +37,6 @@ from fastmcp.server.server import FastMCP
33
37
  from fastmcp.tools.tool import Tool, ToolResult
34
38
  from fastmcp.tools.tool_manager import ToolManager
35
39
  from fastmcp.utilities.logging import get_logger
36
- from fastmcp.utilities.mcp_config import MCPConfig
37
40
 
38
41
  if TYPE_CHECKING:
39
42
  from fastmcp.server import Context
@@ -44,9 +47,9 @@ logger = get_logger(__name__)
44
47
  class ProxyToolManager(ToolManager):
45
48
  """A ToolManager that sources its tools from a remote client in addition to local and mounted tools."""
46
49
 
47
- def __init__(self, client: Client, **kwargs):
50
+ def __init__(self, client_factory: Callable[[], Client], **kwargs):
48
51
  super().__init__(**kwargs)
49
- self.client = client
52
+ self.client_factory = client_factory
50
53
 
51
54
  async def get_tools(self) -> dict[str, Tool]:
52
55
  """Gets the unfiltered tool inventory including local, mounted, and proxy tools."""
@@ -55,13 +58,12 @@ class ProxyToolManager(ToolManager):
55
58
 
56
59
  # Then add proxy tools, but don't overwrite existing ones
57
60
  try:
58
- async with self.client:
59
- client_tools = await self.client.list_tools()
61
+ client = self.client_factory()
62
+ async with client:
63
+ client_tools = await client.list_tools()
60
64
  for tool in client_tools:
61
65
  if tool.name not in all_tools:
62
- all_tools[tool.name] = ProxyTool.from_mcp_tool(
63
- self.client, tool
64
- )
66
+ all_tools[tool.name] = ProxyTool.from_mcp_tool(client, tool)
65
67
  except McpError as e:
66
68
  if e.error.code == METHOD_NOT_FOUND:
67
69
  pass # No tools available from proxy
@@ -82,8 +84,9 @@ class ProxyToolManager(ToolManager):
82
84
  return await super().call_tool(key, arguments)
83
85
  except NotFoundError:
84
86
  # If not found locally, try proxy
85
- async with self.client:
86
- result = await self.client.call_tool(key, arguments)
87
+ client = self.client_factory()
88
+ async with client:
89
+ result = await client.call_tool(key, arguments)
87
90
  return ToolResult(
88
91
  content=result.content,
89
92
  structured_content=result.structured_content,
@@ -93,9 +96,9 @@ class ProxyToolManager(ToolManager):
93
96
  class ProxyResourceManager(ResourceManager):
94
97
  """A ResourceManager that sources its resources from a remote client in addition to local and mounted resources."""
95
98
 
96
- def __init__(self, client: Client, **kwargs):
99
+ def __init__(self, client_factory: Callable[[], Client], **kwargs):
97
100
  super().__init__(**kwargs)
98
- self.client = client
101
+ self.client_factory = client_factory
99
102
 
100
103
  async def get_resources(self) -> dict[str, Resource]:
101
104
  """Gets the unfiltered resource inventory including local, mounted, and proxy resources."""
@@ -104,12 +107,13 @@ class ProxyResourceManager(ResourceManager):
104
107
 
105
108
  # Then add proxy resources, but don't overwrite existing ones
106
109
  try:
107
- async with self.client:
108
- client_resources = await self.client.list_resources()
110
+ client = self.client_factory()
111
+ async with client:
112
+ client_resources = await client.list_resources()
109
113
  for resource in client_resources:
110
114
  if str(resource.uri) not in all_resources:
111
115
  all_resources[str(resource.uri)] = (
112
- ProxyResource.from_mcp_resource(self.client, resource)
116
+ ProxyResource.from_mcp_resource(client, resource)
113
117
  )
114
118
  except McpError as e:
115
119
  if e.error.code == METHOD_NOT_FOUND:
@@ -126,12 +130,13 @@ class ProxyResourceManager(ResourceManager):
126
130
 
127
131
  # Then add proxy templates, but don't overwrite existing ones
128
132
  try:
129
- async with self.client:
130
- client_templates = await self.client.list_resource_templates()
133
+ client = self.client_factory()
134
+ async with client:
135
+ client_templates = await client.list_resource_templates()
131
136
  for template in client_templates:
132
137
  if template.uriTemplate not in all_templates:
133
138
  all_templates[template.uriTemplate] = (
134
- ProxyTemplate.from_mcp_template(self.client, template)
139
+ ProxyTemplate.from_mcp_template(client, template)
135
140
  )
136
141
  except McpError as e:
137
142
  if e.error.code == METHOD_NOT_FOUND:
@@ -158,8 +163,9 @@ class ProxyResourceManager(ResourceManager):
158
163
  return await super().read_resource(uri)
159
164
  except NotFoundError:
160
165
  # If not found locally, try proxy
161
- async with self.client:
162
- result = await self.client.read_resource(uri)
166
+ client = self.client_factory()
167
+ async with client:
168
+ result = await client.read_resource(uri)
163
169
  if isinstance(result[0], TextResourceContents):
164
170
  return result[0].text
165
171
  elif isinstance(result[0], BlobResourceContents):
@@ -171,9 +177,9 @@ class ProxyResourceManager(ResourceManager):
171
177
  class ProxyPromptManager(PromptManager):
172
178
  """A PromptManager that sources its prompts from a remote client in addition to local and mounted prompts."""
173
179
 
174
- def __init__(self, client: Client, **kwargs):
180
+ def __init__(self, client_factory: Callable[[], Client], **kwargs):
175
181
  super().__init__(**kwargs)
176
- self.client = client
182
+ self.client_factory = client_factory
177
183
 
178
184
  async def get_prompts(self) -> dict[str, Prompt]:
179
185
  """Gets the unfiltered prompt inventory including local, mounted, and proxy prompts."""
@@ -182,12 +188,13 @@ class ProxyPromptManager(PromptManager):
182
188
 
183
189
  # Then add proxy prompts, but don't overwrite existing ones
184
190
  try:
185
- async with self.client:
186
- client_prompts = await self.client.list_prompts()
191
+ client = self.client_factory()
192
+ async with client:
193
+ client_prompts = await client.list_prompts()
187
194
  for prompt in client_prompts:
188
195
  if prompt.name not in all_prompts:
189
196
  all_prompts[prompt.name] = ProxyPrompt.from_mcp_prompt(
190
- self.client, prompt
197
+ client, prompt
191
198
  )
192
199
  except McpError as e:
193
200
  if e.error.code == METHOD_NOT_FOUND:
@@ -213,8 +220,9 @@ class ProxyPromptManager(PromptManager):
213
220
  return await super().render_prompt(name, arguments)
214
221
  except NotFoundError:
215
222
  # If not found locally, try proxy
216
- async with self.client:
217
- result = await self.client.get_prompt(name, arguments)
223
+ client = self.client_factory()
224
+ async with client:
225
+ result = await client.get_prompt(name, arguments)
218
226
  return result
219
227
 
220
228
 
@@ -245,7 +253,6 @@ class ProxyTool(Tool):
245
253
  context: Context | None = None,
246
254
  ) -> ToolResult:
247
255
  """Executes the tool by making a call through the client."""
248
- # This is where the remote execution logic lives.
249
256
  async with self._client:
250
257
  result = await self._client.call_tool_mcp(
251
258
  name=self.name,
@@ -267,14 +274,22 @@ class ProxyResource(Resource):
267
274
  _client: Client
268
275
  _value: str | bytes | None = None
269
276
 
270
- def __init__(self, client: Client, *, _value: str | bytes | None = None, **kwargs):
277
+ def __init__(
278
+ self,
279
+ client: Client,
280
+ *,
281
+ _value: str | bytes | None = None,
282
+ **kwargs,
283
+ ):
271
284
  super().__init__(**kwargs)
272
285
  self._client = client
273
286
  self._value = _value
274
287
 
275
288
  @classmethod
276
289
  def from_mcp_resource(
277
- cls, client: Client, mcp_resource: mcp.types.Resource
290
+ cls,
291
+ client: Client,
292
+ mcp_resource: mcp.types.Resource,
278
293
  ) -> ProxyResource:
279
294
  """Factory method to create a ProxyResource from a raw MCP resource schema."""
280
295
  return cls(
@@ -397,24 +412,63 @@ class ProxyPrompt(Prompt):
397
412
  class FastMCPProxy(FastMCP):
398
413
  """
399
414
  A FastMCP server that acts as a proxy to a remote MCP-compliant server.
400
- It uses specialized managers that fulfill requests via an HTTP client.
415
+ It uses specialized managers that fulfill requests via a client factory.
401
416
  """
402
417
 
403
- def __init__(self, client: Client, **kwargs):
418
+ def __init__(
419
+ self,
420
+ client: Client | None = None,
421
+ *,
422
+ client_factory: Callable[[], Client] | None = None,
423
+ **kwargs,
424
+ ):
404
425
  """
405
426
  Initializes the proxy server.
406
427
 
428
+ FastMCPProxy requires explicit session management via client_factory.
429
+ Use FastMCP.as_proxy() for convenience with automatic session strategy.
430
+
407
431
  Args:
408
- client: The FastMCP client connected to the backend server.
432
+ client: [DEPRECATED] A Client instance. Use client_factory instead for explicit
433
+ session management. When provided, a client_factory will be automatically
434
+ created that provides session isolation for backwards compatibility.
435
+ client_factory: A callable that returns a Client instance when called.
436
+ This gives you full control over session creation and reuse.
409
437
  **kwargs: Additional settings for the FastMCP server.
410
438
  """
439
+
411
440
  super().__init__(**kwargs)
412
- self.client = client
441
+
442
+ # Handle client and client_factory parameters
443
+ if client is not None and client_factory is not None:
444
+ raise ValueError("Cannot specify both 'client' and 'client_factory'")
445
+
446
+ if client is not None:
447
+ # Deprecated in 2.10.3
448
+ if fastmcp.settings.deprecation_warnings:
449
+ warnings.warn(
450
+ "Passing 'client' to FastMCPProxy is deprecated. Use 'client_factory' instead for explicit session management. "
451
+ "For automatic session strategy, use FastMCP.as_proxy().",
452
+ DeprecationWarning,
453
+ stacklevel=2,
454
+ )
455
+
456
+ # Create a factory that provides session isolation for backwards compatibility
457
+ def deprecated_client_factory():
458
+ return client.new()
459
+
460
+ self.client_factory = deprecated_client_factory
461
+ elif client_factory is not None:
462
+ self.client_factory = client_factory
463
+ else:
464
+ raise ValueError("Must specify 'client_factory'")
413
465
 
414
466
  # Replace the default managers with our specialized proxy managers.
415
- self._tool_manager = ProxyToolManager(client=self.client)
416
- self._resource_manager = ProxyResourceManager(client=self.client)
417
- self._prompt_manager = ProxyPromptManager(client=self.client)
467
+ self._tool_manager = ProxyToolManager(client_factory=self.client_factory)
468
+ self._resource_manager = ProxyResourceManager(
469
+ client_factory=self.client_factory
470
+ )
471
+ self._prompt_manager = ProxyPromptManager(client_factory=self.client_factory)
418
472
 
419
473
 
420
474
  async def default_proxy_roots_handler(
@@ -435,15 +489,14 @@ class ProxyClient(Client[ClientTransportT]):
435
489
 
436
490
  def __init__(
437
491
  self,
438
- transport: (
439
- ClientTransportT
440
- | FastMCP
441
- | AnyUrl
442
- | Path
443
- | MCPConfig
444
- | dict[str, Any]
445
- | str
446
- ),
492
+ transport: ClientTransportT
493
+ | FastMCP
494
+ | FastMCP1Server
495
+ | AnyUrl
496
+ | Path
497
+ | MCPConfig
498
+ | dict[str, Any]
499
+ | str,
447
500
  **kwargs,
448
501
  ):
449
502
  if "roots" not in kwargs:
@@ -456,7 +509,7 @@ class ProxyClient(Client[ClientTransportT]):
456
509
  kwargs["log_handler"] = ProxyClient.default_log_handler
457
510
  if "progress_handler" not in kwargs:
458
511
  kwargs["progress_handler"] = ProxyClient.default_progress_handler
459
- super().__init__(transport, **kwargs)
512
+ super().__init__(**kwargs | dict(transport=transport))
460
513
 
461
514
  @classmethod
462
515
  async def default_sampling_handler(
fastmcp/server/server.py CHANGED
@@ -43,6 +43,7 @@ from starlette.routing import BaseRoute, Route
43
43
  import fastmcp
44
44
  import fastmcp.server
45
45
  from fastmcp.exceptions import DisabledError, NotFoundError
46
+ from fastmcp.mcp_config import MCPConfig
46
47
  from fastmcp.prompts import Prompt, PromptManager
47
48
  from fastmcp.prompts.prompt import FunctionPrompt
48
49
  from fastmcp.resources import Resource, ResourceManager
@@ -63,7 +64,6 @@ from fastmcp.utilities.cache import TimedCache
63
64
  from fastmcp.utilities.cli import log_server_banner
64
65
  from fastmcp.utilities.components import FastMCPComponent
65
66
  from fastmcp.utilities.logging import get_logger
66
- from fastmcp.utilities.mcp_config import MCPConfig
67
67
  from fastmcp.utilities.types import NotSet, NotSetT
68
68
 
69
69
  if TYPE_CHECKING:
@@ -1931,10 +1931,39 @@ class FastMCP(Generic[LifespanResultT]):
1931
1931
 
1932
1932
  if isinstance(backend, Client):
1933
1933
  client = backend
1934
+ # Session strategy based on client connection state:
1935
+ # - Connected clients: reuse existing session for all requests
1936
+ # - Disconnected clients: create fresh sessions per request for isolation
1937
+ if client.is_connected():
1938
+ from fastmcp.utilities.logging import get_logger
1939
+
1940
+ logger = get_logger(__name__)
1941
+ logger.info(
1942
+ "Proxy detected connected client - reusing existing session for all requests. "
1943
+ "This may cause context mixing in concurrent scenarios."
1944
+ )
1945
+
1946
+ # Reuse sessions - return the same client instance
1947
+ def reuse_client_factory():
1948
+ return client
1949
+
1950
+ client_factory = reuse_client_factory
1951
+ else:
1952
+ # Fresh sessions per request
1953
+ def fresh_client_factory():
1954
+ return client.new()
1955
+
1956
+ client_factory = fresh_client_factory
1934
1957
  else:
1935
- client = ProxyClient(backend)
1958
+ base_client = ProxyClient(backend)
1959
+
1960
+ # Fresh client created from transport - use fresh sessions per request
1961
+ def proxy_client_factory():
1962
+ return base_client.new()
1963
+
1964
+ client_factory = proxy_client_factory
1936
1965
 
1937
- return FastMCPProxy(client=client, **settings)
1966
+ return FastMCPProxy(client_factory=client_factory, **settings)
1938
1967
 
1939
1968
  @classmethod
1940
1969
  def from_client(
fastmcp/tools/tool.py CHANGED
@@ -46,7 +46,7 @@ class _UnserializableType:
46
46
 
47
47
 
48
48
  def default_serializer(data: Any) -> str:
49
- return pydantic_core.to_json(data, fallback=str, indent=2).decode()
49
+ return pydantic_core.to_json(data, fallback=str).decode()
50
50
 
51
51
 
52
52
  class ToolResult:
@@ -434,6 +434,7 @@ def _convert_to_content(
434
434
  _process_as_single_item: bool = False,
435
435
  ) -> list[ContentBlock]:
436
436
  """Convert a result to a sequence of content objects."""
437
+
437
438
  if result is None:
438
439
  return []
439
440
 
@@ -467,7 +468,7 @@ def _convert_to_content(
467
468
 
468
469
  if other_content:
469
470
  other_content = _convert_to_content(
470
- other_content[0] if len(other_content) == 1 else other_content,
471
+ other_content,
471
472
  serializer=serializer,
472
473
  _process_as_single_item=True,
473
474
  )
@@ -9,7 +9,7 @@ from typing import Any, Literal
9
9
  from mcp.types import ToolAnnotations
10
10
  from pydantic import ConfigDict
11
11
 
12
- from fastmcp.tools.tool import ParsedFunction, Tool, ToolResult
12
+ from fastmcp.tools.tool import ParsedFunction, Tool, ToolResult, _convert_to_content
13
13
  from fastmcp.utilities.logging import get_logger
14
14
  from fastmcp.utilities.types import NotSet, NotSetT, get_cached_typeadapter
15
15
 
@@ -233,7 +233,6 @@ class TransformedTool(Tool):
233
233
  Returns:
234
234
  ToolResult object containing content and optional structured output.
235
235
  """
236
- from fastmcp.tools.tool import _convert_to_content
237
236
 
238
237
  # Fill in missing arguments with schema defaults to ensure
239
238
  # ArgTransform defaults take precedence over function defaults
@@ -274,7 +273,6 @@ class TransformedTool(Tool):
274
273
  if isinstance(result, ToolResult):
275
274
  if self.output_schema is None:
276
275
  # Check if this is from a custom function that returns ToolResult
277
- import inspect
278
276
 
279
277
  return_annotation = inspect.signature(self.fn).return_annotation
280
278
  if return_annotation is ToolResult:
@@ -298,7 +296,6 @@ class TransformedTool(Tool):
298
296
  return result
299
297
 
300
298
  # Otherwise convert to content and create ToolResult with proper structured content
301
- from fastmcp.tools.tool import _convert_to_content
302
299
 
303
300
  unstructured_result = _convert_to_content(
304
301
  result, serializer=self.serializer
@@ -433,8 +430,6 @@ class TransformedTool(Tool):
433
430
  final_output_schema = parsed_fn.output_schema
434
431
  if final_output_schema is None:
435
432
  # Check if function returns ToolResult - if so, don't fall back to parent
436
- import inspect
437
-
438
433
  return_annotation = inspect.signature(
439
434
  transform_fn
440
435
  ).return_annotation
@@ -553,6 +548,7 @@ class TransformedTool(Tool):
553
548
  """
554
549
 
555
550
  # Build transformed schema and mapping
551
+ parent_defs = parent_tool.parameters.get("$defs", {})
556
552
  parent_props = parent_tool.parameters.get("properties", {}).copy()
557
553
  parent_required = set(parent_tool.parameters.get("required", []))
558
554
 
@@ -608,6 +604,9 @@ class TransformedTool(Tool):
608
604
  "required": list(new_required),
609
605
  }
610
606
 
607
+ if parent_defs:
608
+ schema["$defs"] = parent_defs
609
+
611
610
  # Create forwarding function that closes over everything it needs
612
611
  async def _forward(**kwargs):
613
612
  # Validate arguments
@@ -67,13 +67,24 @@ def _prune_unused_defs(schema: dict) -> dict:
67
67
  walk(value, current_def=def_name)
68
68
 
69
69
  # Figure out what defs were referenced directly or recursively
70
- def def_is_referenced(def_name):
70
+ def def_is_referenced(def_name, parent_def_names: set[str] | None = None):
71
71
  if def_name in root_defs:
72
72
  return True
73
73
  references = referenced_by.get(def_name)
74
74
  if references:
75
- for reference in references:
76
- if def_is_referenced(reference):
75
+ if parent_def_names is None:
76
+ parent_def_names = set()
77
+
78
+ # Handle recursion by excluding references already present in parent references
79
+ parent_def_names = parent_def_names | {def_name}
80
+ valid_references = [
81
+ reference
82
+ for reference in references
83
+ if reference not in parent_def_names
84
+ ]
85
+
86
+ for reference in valid_references:
87
+ if def_is_referenced(reference, parent_def_names):
77
88
  return True
78
89
  return False
79
90
 
@@ -152,6 +152,7 @@ __all__ = [
152
152
  "ParameterLocation",
153
153
  "JsonSchema",
154
154
  "parse_openapi_to_http_routes",
155
+ "extract_output_schema_from_responses",
155
156
  ]
156
157
 
157
158
  # Type variables for generic parser
@@ -1107,3 +1108,94 @@ def _combine_schemas(route: HTTPRoute) -> dict[str, Any]:
1107
1108
  result = compress_schema(result)
1108
1109
 
1109
1110
  return result
1111
+
1112
+
1113
+ def extract_output_schema_from_responses(
1114
+ responses: dict[str, ResponseInfo], schema_definitions: dict[str, Any] | None = None
1115
+ ) -> dict[str, Any] | None:
1116
+ """
1117
+ Extract output schema from OpenAPI responses for use as MCP tool output schema.
1118
+
1119
+ This function finds the first successful response (200, 201, 202, 204) with a
1120
+ JSON-compatible content type and extracts its schema. If the schema is not an
1121
+ object type, it wraps it to comply with MCP requirements.
1122
+
1123
+ Args:
1124
+ responses: Dictionary of ResponseInfo objects keyed by status code
1125
+ schema_definitions: Optional schema definitions to include in the output schema
1126
+
1127
+ Returns:
1128
+ dict: MCP-compliant output schema with potential wrapping, or None if no suitable schema found
1129
+ """
1130
+ if not responses:
1131
+ return None
1132
+
1133
+ # Priority order for success status codes
1134
+ success_codes = ["200", "201", "202", "204"]
1135
+
1136
+ # Find the first successful response
1137
+ response_info = None
1138
+ for status_code in success_codes:
1139
+ if status_code in responses:
1140
+ response_info = responses[status_code]
1141
+ break
1142
+
1143
+ # If no explicit success codes, try any 2xx response
1144
+ if response_info is None:
1145
+ for status_code, resp_info in responses.items():
1146
+ if status_code.startswith("2"):
1147
+ response_info = resp_info
1148
+ break
1149
+
1150
+ if response_info is None or not response_info.content_schema:
1151
+ return None
1152
+
1153
+ # Prefer application/json, then fall back to other JSON-compatible types
1154
+ json_compatible_types = [
1155
+ "application/json",
1156
+ "application/vnd.api+json",
1157
+ "application/hal+json",
1158
+ "application/ld+json",
1159
+ "text/json",
1160
+ ]
1161
+
1162
+ schema = None
1163
+ for content_type in json_compatible_types:
1164
+ if content_type in response_info.content_schema:
1165
+ schema = response_info.content_schema[content_type]
1166
+ break
1167
+
1168
+ # If no JSON-compatible type found, try the first available content type
1169
+ if schema is None and response_info.content_schema:
1170
+ first_content_type = next(iter(response_info.content_schema))
1171
+ schema = response_info.content_schema[first_content_type]
1172
+ logger.debug(
1173
+ f"Using non-JSON content type for output schema: {first_content_type}"
1174
+ )
1175
+
1176
+ if not schema or not isinstance(schema, dict):
1177
+ return None
1178
+
1179
+ # Clean and copy the schema
1180
+ output_schema = schema.copy()
1181
+
1182
+ # MCP requires output schemas to be objects. If this schema is not an object,
1183
+ # we need to wrap it similar to how ParsedFunction.from_function() does it
1184
+ if output_schema.get("type") != "object":
1185
+ # Create a wrapped schema that contains the original schema under a "result" key
1186
+ wrapped_schema = {
1187
+ "type": "object",
1188
+ "properties": {"result": output_schema},
1189
+ "required": ["result"],
1190
+ "x-fastmcp-wrap-result": True,
1191
+ }
1192
+ output_schema = wrapped_schema
1193
+
1194
+ # Add schema definitions if available
1195
+ if schema_definitions:
1196
+ output_schema["$defs"] = schema_definitions
1197
+
1198
+ # Use compress_schema to remove unused definitions
1199
+ output_schema = compress_schema(output_schema)
1200
+
1201
+ return output_schema
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastmcp
3
- Version: 2.10.2
4
- Summary: The fast, Pythonic way to build MCP servers.
3
+ Version: 2.10.4
4
+ Summary: The fast, Pythonic way to build MCP servers and clients.
5
5
  Project-URL: Homepage, https://gofastmcp.com
6
6
  Project-URL: Repository, https://github.com/jlowin/fastmcp
7
7
  Project-URL: Documentation, https://gofastmcp.com
@@ -18,14 +18,15 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
18
  Classifier: Typing :: Typed
19
19
  Requires-Python: >=3.10
20
20
  Requires-Dist: authlib>=1.5.2
21
+ Requires-Dist: cyclopts>=3.0.0
21
22
  Requires-Dist: exceptiongroup>=1.2.2
22
23
  Requires-Dist: httpx>=0.28.1
23
24
  Requires-Dist: mcp>=1.10.0
24
25
  Requires-Dist: openapi-pydantic>=0.5.1
25
26
  Requires-Dist: pydantic[email]>=2.11.7
27
+ Requires-Dist: pyperclip>=1.9.0
26
28
  Requires-Dist: python-dotenv>=1.1.0
27
29
  Requires-Dist: rich>=13.9.4
28
- Requires-Dist: typer>=0.15.2
29
30
  Provides-Extra: websockets
30
31
  Requires-Dist: websockets>=15.0.1; extra == 'websockets'
31
32
  Description-Content-Type: text/markdown