fastmcp 2.8.1__py3-none-any.whl → 2.9.0__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 (38) hide show
  1. fastmcp/cli/cli.py +99 -1
  2. fastmcp/cli/run.py +1 -3
  3. fastmcp/client/auth/oauth.py +1 -2
  4. fastmcp/client/client.py +21 -5
  5. fastmcp/client/transports.py +17 -2
  6. fastmcp/contrib/mcp_mixin/README.md +79 -2
  7. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -0
  8. fastmcp/prompts/prompt.py +91 -11
  9. fastmcp/prompts/prompt_manager.py +119 -43
  10. fastmcp/resources/resource.py +11 -1
  11. fastmcp/resources/resource_manager.py +249 -76
  12. fastmcp/resources/template.py +27 -1
  13. fastmcp/server/auth/providers/bearer.py +32 -10
  14. fastmcp/server/context.py +41 -2
  15. fastmcp/server/http.py +8 -0
  16. fastmcp/server/middleware/__init__.py +6 -0
  17. fastmcp/server/middleware/error_handling.py +206 -0
  18. fastmcp/server/middleware/logging.py +165 -0
  19. fastmcp/server/middleware/middleware.py +236 -0
  20. fastmcp/server/middleware/rate_limiting.py +231 -0
  21. fastmcp/server/middleware/timing.py +156 -0
  22. fastmcp/server/proxy.py +250 -140
  23. fastmcp/server/server.py +320 -242
  24. fastmcp/settings.py +2 -2
  25. fastmcp/tools/tool.py +6 -2
  26. fastmcp/tools/tool_manager.py +114 -45
  27. fastmcp/utilities/components.py +22 -2
  28. fastmcp/utilities/inspect.py +326 -0
  29. fastmcp/utilities/json_schema.py +67 -23
  30. fastmcp/utilities/mcp_config.py +13 -7
  31. fastmcp/utilities/openapi.py +5 -3
  32. fastmcp/utilities/tests.py +1 -1
  33. fastmcp/utilities/types.py +90 -1
  34. {fastmcp-2.8.1.dist-info → fastmcp-2.9.0.dist-info}/METADATA +2 -2
  35. {fastmcp-2.8.1.dist-info → fastmcp-2.9.0.dist-info}/RECORD +38 -31
  36. {fastmcp-2.8.1.dist-info → fastmcp-2.9.0.dist-info}/WHEEL +0 -0
  37. {fastmcp-2.8.1.dist-info → fastmcp-2.9.0.dist-info}/entry_points.txt +0 -0
  38. {fastmcp-2.8.1.dist-info → fastmcp-2.9.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/settings.py CHANGED
@@ -192,9 +192,9 @@ class Settings(BaseSettings):
192
192
  # HTTP settings
193
193
  host: str = "127.0.0.1"
194
194
  port: int = 8000
195
- sse_path: str = "/sse"
195
+ sse_path: str = "/sse/"
196
196
  message_path: str = "/messages/"
197
- streamable_http_path: str = "/mcp"
197
+ streamable_http_path: str = "/mcp/"
198
198
  debug: bool = False
199
199
 
200
200
  # error handling
fastmcp/tools/tool.py CHANGED
@@ -18,6 +18,7 @@ from fastmcp.utilities.json_schema import compress_schema
18
18
  from fastmcp.utilities.logging import get_logger
19
19
  from fastmcp.utilities.types import (
20
20
  Audio,
21
+ File,
21
22
  Image,
22
23
  MCPContent,
23
24
  find_kwarg_by_type,
@@ -231,7 +232,7 @@ class ParsedFunction:
231
232
 
232
233
  # collect name and doc before we potentially modify the function
233
234
  fn_name = getattr(fn, "__name__", None) or fn.__class__.__name__
234
- fn_doc = fn.__doc__
235
+ fn_doc = inspect.getdoc(fn)
235
236
 
236
237
  # if the fn is a callable class, we need to get the __call__ method from here out
237
238
  if not inspect.isroutine(fn):
@@ -277,6 +278,9 @@ def _convert_to_content(
277
278
  elif isinstance(result, Audio):
278
279
  return [result.to_audio_content()]
279
280
 
281
+ elif isinstance(result, File):
282
+ return [result.to_resource_content()]
283
+
280
284
  if isinstance(result, list | tuple) and not _process_as_single_item:
281
285
  # if the result is a list, then it could either be a list of MCP types,
282
286
  # or a "regular" list that the tool is returning, or a mix of both.
@@ -288,7 +292,7 @@ def _convert_to_content(
288
292
  other_content = []
289
293
 
290
294
  for item in result:
291
- if isinstance(item, MCPContent | Image | Audio):
295
+ if isinstance(item, MCPContent | Image | Audio | File):
292
296
  mcp_types.append(_convert_to_content(item)[0])
293
297
  else:
294
298
  other_content.append(item)
@@ -1,4 +1,4 @@
1
- from __future__ import annotations as _annotations
1
+ from __future__ import annotations
2
2
 
3
3
  import warnings
4
4
  from collections.abc import Callable
@@ -14,7 +14,7 @@ from fastmcp.utilities.logging import get_logger
14
14
  from fastmcp.utilities.types import MCPContent
15
15
 
16
16
  if TYPE_CHECKING:
17
- pass
17
+ from fastmcp.server.server import MountedServer
18
18
 
19
19
  logger = get_logger(__name__)
20
20
 
@@ -28,6 +28,7 @@ class ToolManager:
28
28
  mask_error_details: bool | None = None,
29
29
  ):
30
30
  self._tools: dict[str, Tool] = {}
31
+ self._mounted_servers: list[MountedServer] = []
31
32
  self.mask_error_details = mask_error_details or settings.mask_error_details
32
33
 
33
34
  # Default to "warn" if None is provided
@@ -42,23 +43,72 @@ class ToolManager:
42
43
 
43
44
  self.duplicate_behavior = duplicate_behavior
44
45
 
45
- def has_tool(self, key: str) -> bool:
46
+ def mount(self, server: MountedServer) -> None:
47
+ """Adds a mounted server as a source for tools."""
48
+ self._mounted_servers.append(server)
49
+
50
+ async def _load_tools(self, *, via_server: bool = False) -> dict[str, Tool]:
51
+ """
52
+ The single, consolidated recursive method for fetching tools. The 'via_server'
53
+ parameter determines the communication path.
54
+
55
+ - via_server=False: Manager-to-manager path for complete, unfiltered inventory
56
+ - via_server=True: Server-to-server path for filtered MCP requests
57
+ """
58
+ all_tools: dict[str, Tool] = {}
59
+
60
+ for mounted in self._mounted_servers:
61
+ try:
62
+ if via_server:
63
+ # Use the server-to-server filtered path
64
+ child_results = await mounted.server._list_tools()
65
+ else:
66
+ # Use the manager-to-manager unfiltered path
67
+ child_results = await mounted.server._tool_manager.list_tools()
68
+
69
+ # The combination logic is the same for both paths
70
+ child_dict = {t.key: t for t in child_results}
71
+ if mounted.prefix:
72
+ for tool in child_dict.values():
73
+ prefixed_tool = tool.with_key(f"{mounted.prefix}_{tool.key}")
74
+ all_tools[prefixed_tool.key] = prefixed_tool
75
+ else:
76
+ all_tools.update(child_dict)
77
+ except Exception as e:
78
+ # Skip failed mounts silently, matches existing behavior
79
+ logger.warning(
80
+ f"Failed to get tools from mounted server '{mounted.prefix}': {e}"
81
+ )
82
+ continue
83
+
84
+ # Finally, add local tools, which always take precedence
85
+ all_tools.update(self._tools)
86
+ return all_tools
87
+
88
+ async def has_tool(self, key: str) -> bool:
46
89
  """Check if a tool exists."""
47
- return key in self._tools
90
+ tools = await self.get_tools()
91
+ return key in tools
48
92
 
49
- def get_tool(self, key: str) -> Tool:
93
+ async def get_tool(self, key: str) -> Tool:
50
94
  """Get tool by key."""
51
- if key in self._tools:
52
- return self._tools[key]
53
- raise NotFoundError(f"Unknown tool: {key}")
95
+ tools = await self.get_tools()
96
+ if key in tools:
97
+ return tools[key]
98
+ raise NotFoundError(f"Tool {key!r} not found")
54
99
 
55
- def get_tools(self) -> dict[str, Tool]:
56
- """Get all registered tools, indexed by registered key."""
57
- return self._tools
100
+ async def get_tools(self) -> dict[str, Tool]:
101
+ """
102
+ Gets the complete, unfiltered inventory of all tools.
103
+ """
104
+ return await self._load_tools(via_server=False)
58
105
 
59
- def list_tools(self) -> list[Tool]:
60
- """List all registered tools."""
61
- return list(self.get_tools().values())
106
+ async def list_tools(self) -> list[Tool]:
107
+ """
108
+ Lists all tools, applying protocol filtering.
109
+ """
110
+ tools_dict = await self._load_tools(via_server=True)
111
+ return list(tools_dict.values())
62
112
 
63
113
  def add_tool_from_fn(
64
114
  self,
@@ -89,22 +139,21 @@ class ToolManager:
89
139
  )
90
140
  return self.add_tool(tool)
91
141
 
92
- def add_tool(self, tool: Tool, key: str | None = None) -> Tool:
142
+ def add_tool(self, tool: Tool) -> Tool:
93
143
  """Register a tool with the server."""
94
- key = key or tool.name
95
- existing = self._tools.get(key)
144
+ existing = self._tools.get(tool.key)
96
145
  if existing:
97
146
  if self.duplicate_behavior == "warn":
98
- logger.warning(f"Tool already exists: {key}")
99
- self._tools[key] = tool
147
+ logger.warning(f"Tool already exists: {tool.key}")
148
+ self._tools[tool.key] = tool
100
149
  elif self.duplicate_behavior == "replace":
101
- self._tools[key] = tool
150
+ self._tools[tool.key] = tool
102
151
  elif self.duplicate_behavior == "error":
103
- raise ValueError(f"Tool already exists: {key}")
152
+ raise ValueError(f"Tool already exists: {tool.key}")
104
153
  elif self.duplicate_behavior == "ignore":
105
154
  return existing
106
155
  else:
107
- self._tools[key] = tool
156
+ self._tools[tool.key] = tool
108
157
  return tool
109
158
 
110
159
  def remove_tool(self, key: str) -> None:
@@ -119,28 +168,48 @@ class ToolManager:
119
168
  if key in self._tools:
120
169
  del self._tools[key]
121
170
  else:
122
- raise NotFoundError(f"Unknown tool: {key}")
171
+ raise NotFoundError(f"Tool {key!r} not found")
123
172
 
124
173
  async def call_tool(self, key: str, arguments: dict[str, Any]) -> list[MCPContent]:
125
- """Call a tool by name with arguments."""
126
- tool = self.get_tool(key)
127
- if not tool:
128
- raise NotFoundError(f"Unknown tool: {key}")
129
-
130
- try:
131
- return await tool.run(arguments)
132
-
133
- # raise ToolErrors as-is
134
- except ToolError as e:
135
- logger.exception(f"Error calling tool {key!r}: {e}")
136
- raise e
137
-
138
- # Handle other exceptions
139
- except Exception as e:
140
- logger.exception(f"Error calling tool {key!r}: {e}")
141
- if self.mask_error_details:
142
- # Mask internal details
143
- raise ToolError(f"Error calling tool {key!r}") from e
144
- else:
145
- # Include original error details
146
- raise ToolError(f"Error calling tool {key!r}: {e}") from e
174
+ """
175
+ Internal API for servers: Finds and calls a tool, respecting the
176
+ filtered protocol path.
177
+ """
178
+ # 1. Check local tools first. The server will have already applied its filter.
179
+ if key in self._tools:
180
+ tool = await self.get_tool(key)
181
+ if not tool:
182
+ raise NotFoundError(f"Tool {key!r} not found")
183
+
184
+ try:
185
+ return await tool.run(arguments)
186
+
187
+ # raise ToolErrors as-is
188
+ except ToolError as e:
189
+ logger.exception(f"Error calling tool {key!r}: {e}")
190
+ raise e
191
+
192
+ # Handle other exceptions
193
+ except Exception as e:
194
+ logger.exception(f"Error calling tool {key!r}: {e}")
195
+ if self.mask_error_details:
196
+ # Mask internal details
197
+ raise ToolError(f"Error calling tool {key!r}") from e
198
+ else:
199
+ # Include original error details
200
+ raise ToolError(f"Error calling tool {key!r}: {e}") from e
201
+
202
+ # 2. Check mounted servers using the filtered protocol path.
203
+ for mounted in reversed(self._mounted_servers):
204
+ tool_key = key
205
+ if mounted.prefix:
206
+ if key.startswith(f"{mounted.prefix}_"):
207
+ tool_key = key.removeprefix(f"{mounted.prefix}_")
208
+ else:
209
+ continue
210
+ try:
211
+ return await mounted.server._call_tool(tool_key, arguments)
212
+ except NotFoundError:
213
+ continue
214
+
215
+ raise NotFoundError(f"Tool {key!r} not found.")
@@ -1,7 +1,8 @@
1
1
  from collections.abc import Sequence
2
- from typing import Annotated, TypeVar
2
+ from typing import Annotated, Any, TypeVar
3
3
 
4
- from pydantic import BeforeValidator, Field
4
+ from pydantic import BeforeValidator, Field, PrivateAttr
5
+ from typing_extensions import Self
5
6
 
6
7
  from fastmcp.utilities.types import FastMCPBaseModel
7
8
 
@@ -37,6 +38,25 @@ class FastMCPComponent(FastMCPBaseModel):
37
38
  description="Whether the component is enabled.",
38
39
  )
39
40
 
41
+ _key: str | None = PrivateAttr()
42
+
43
+ def __init__(self, *, key: str | None = None, **kwargs: Any) -> None:
44
+ super().__init__(**kwargs)
45
+ self._key = key
46
+
47
+ @property
48
+ def key(self) -> str:
49
+ """
50
+ The key of the component. This is used for internal bookkeeping
51
+ and may reflect e.g. prefixes or other identifiers. You should not depend on
52
+ keys having a certain value, as the same tool loaded from different
53
+ hierarchies of servers may have different keys.
54
+ """
55
+ return self._key or self.name
56
+
57
+ def with_key(self, key: str) -> Self:
58
+ return self.model_copy(update={"_key": key})
59
+
40
60
  def __eq__(self, other: object) -> bool:
41
61
  if type(self) is not type(other):
42
62
  return False
@@ -0,0 +1,326 @@
1
+ """Utilities for inspecting FastMCP instances."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.metadata
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+ from mcp.server.fastmcp import FastMCP as FastMCP1x
10
+
11
+ import fastmcp
12
+ from fastmcp.server.server import FastMCP
13
+
14
+
15
+ @dataclass
16
+ class ToolInfo:
17
+ """Information about a tool."""
18
+
19
+ key: str
20
+ name: str
21
+ description: str | None
22
+ input_schema: dict[str, Any]
23
+ annotations: dict[str, Any] | None = None
24
+ tags: list[str] | None = None
25
+ enabled: bool | None = None
26
+
27
+
28
+ @dataclass
29
+ class PromptInfo:
30
+ """Information about a prompt."""
31
+
32
+ key: str
33
+ name: str
34
+ description: str | None
35
+ arguments: list[dict[str, Any]] | None = None
36
+ tags: list[str] | None = None
37
+ enabled: bool | None = None
38
+
39
+
40
+ @dataclass
41
+ class ResourceInfo:
42
+ """Information about a resource."""
43
+
44
+ key: str
45
+ uri: str
46
+ name: str | None
47
+ description: str | None
48
+ mime_type: str | None = None
49
+ tags: list[str] | None = None
50
+ enabled: bool | None = None
51
+
52
+
53
+ @dataclass
54
+ class TemplateInfo:
55
+ """Information about a resource template."""
56
+
57
+ key: str
58
+ uri_template: str
59
+ name: str | None
60
+ description: str | None
61
+ mime_type: str | None = None
62
+ tags: list[str] | None = None
63
+ enabled: bool | None = None
64
+
65
+
66
+ @dataclass
67
+ class FastMCPInfo:
68
+ """Information extracted from a FastMCP instance."""
69
+
70
+ name: str
71
+ instructions: str | None
72
+ fastmcp_version: str
73
+ mcp_version: str
74
+ server_version: str
75
+ tools: list[ToolInfo]
76
+ prompts: list[PromptInfo]
77
+ resources: list[ResourceInfo]
78
+ templates: list[TemplateInfo]
79
+ capabilities: dict[str, Any]
80
+
81
+
82
+ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
83
+ """Extract information from a FastMCP v2.x instance.
84
+
85
+ Args:
86
+ mcp: The FastMCP v2.x instance to inspect
87
+
88
+ Returns:
89
+ FastMCPInfo dataclass containing the extracted information
90
+ """
91
+ # Get all the components using FastMCP2's direct methods
92
+ tools_dict = await mcp.get_tools()
93
+ prompts_dict = await mcp.get_prompts()
94
+ resources_dict = await mcp.get_resources()
95
+ templates_dict = await mcp.get_resource_templates()
96
+
97
+ # Extract detailed tool information
98
+ tool_infos = []
99
+ for key, tool in tools_dict.items():
100
+ # Convert to MCP tool to get input schema
101
+ mcp_tool = tool.to_mcp_tool(name=key)
102
+ tool_infos.append(
103
+ ToolInfo(
104
+ key=key,
105
+ name=tool.name or key,
106
+ description=tool.description,
107
+ input_schema=mcp_tool.inputSchema if mcp_tool.inputSchema else {},
108
+ annotations=tool.annotations.model_dump() if tool.annotations else None,
109
+ tags=list(tool.tags) if tool.tags else None,
110
+ enabled=tool.enabled,
111
+ )
112
+ )
113
+
114
+ # Extract detailed prompt information
115
+ prompt_infos = []
116
+ for key, prompt in prompts_dict.items():
117
+ prompt_infos.append(
118
+ PromptInfo(
119
+ key=key,
120
+ name=prompt.name or key,
121
+ description=prompt.description,
122
+ arguments=[arg.model_dump() for arg in prompt.arguments]
123
+ if prompt.arguments
124
+ else None,
125
+ tags=list(prompt.tags) if prompt.tags else None,
126
+ enabled=prompt.enabled,
127
+ )
128
+ )
129
+
130
+ # Extract detailed resource information
131
+ resource_infos = []
132
+ for key, resource in resources_dict.items():
133
+ resource_infos.append(
134
+ ResourceInfo(
135
+ key=key,
136
+ uri=key, # For v2, key is the URI
137
+ name=resource.name,
138
+ description=resource.description,
139
+ mime_type=resource.mime_type,
140
+ tags=list(resource.tags) if resource.tags else None,
141
+ enabled=resource.enabled,
142
+ )
143
+ )
144
+
145
+ # Extract detailed template information
146
+ template_infos = []
147
+ for key, template in templates_dict.items():
148
+ template_infos.append(
149
+ TemplateInfo(
150
+ key=key,
151
+ uri_template=key, # For v2, key is the URI template
152
+ name=template.name,
153
+ description=template.description,
154
+ mime_type=template.mime_type,
155
+ tags=list(template.tags) if template.tags else None,
156
+ enabled=template.enabled,
157
+ )
158
+ )
159
+
160
+ # Basic MCP capabilities that FastMCP supports
161
+ capabilities = {
162
+ "tools": {"listChanged": True},
163
+ "resources": {"subscribe": False, "listChanged": False},
164
+ "prompts": {"listChanged": False},
165
+ "logging": {},
166
+ }
167
+
168
+ return FastMCPInfo(
169
+ name=mcp.name,
170
+ instructions=mcp.instructions,
171
+ fastmcp_version=fastmcp.__version__,
172
+ mcp_version=importlib.metadata.version("mcp"),
173
+ server_version=fastmcp.__version__, # v2.x uses FastMCP version
174
+ tools=tool_infos,
175
+ prompts=prompt_infos,
176
+ resources=resource_infos,
177
+ templates=template_infos,
178
+ capabilities=capabilities,
179
+ )
180
+
181
+
182
+ async def inspect_fastmcp_v1(mcp: Any) -> FastMCPInfo:
183
+ """Extract information from a FastMCP v1.x instance using a Client.
184
+
185
+ Args:
186
+ mcp: The FastMCP v1.x instance to inspect
187
+
188
+ Returns:
189
+ FastMCPInfo dataclass containing the extracted information
190
+ """
191
+ from fastmcp import Client
192
+
193
+ # Use a client to interact with the FastMCP1x server
194
+ async with Client(mcp) as client:
195
+ # Get components via client calls (these return MCP objects)
196
+ mcp_tools = await client.list_tools()
197
+ mcp_prompts = await client.list_prompts()
198
+ mcp_resources = await client.list_resources()
199
+
200
+ # Try to get resource templates (FastMCP 1.x does have templates)
201
+ try:
202
+ mcp_templates = await client.list_resource_templates()
203
+ except Exception:
204
+ mcp_templates = []
205
+
206
+ # Extract detailed tool information from MCP Tool objects
207
+ tool_infos = []
208
+ for mcp_tool in mcp_tools:
209
+ # Extract annotations if they exist
210
+ annotations = None
211
+ if hasattr(mcp_tool, "annotations") and mcp_tool.annotations:
212
+ if hasattr(mcp_tool.annotations, "model_dump"):
213
+ annotations = mcp_tool.annotations.model_dump()
214
+ elif isinstance(mcp_tool.annotations, dict):
215
+ annotations = mcp_tool.annotations
216
+ else:
217
+ annotations = None
218
+
219
+ tool_infos.append(
220
+ ToolInfo(
221
+ key=mcp_tool.name, # For 1.x, key and name are the same
222
+ name=mcp_tool.name,
223
+ description=mcp_tool.description,
224
+ input_schema=mcp_tool.inputSchema if mcp_tool.inputSchema else {},
225
+ annotations=annotations,
226
+ tags=None, # 1.x doesn't have tags
227
+ enabled=None, # 1.x doesn't have enabled field
228
+ )
229
+ )
230
+
231
+ # Extract detailed prompt information from MCP Prompt objects
232
+ prompt_infos = []
233
+ for mcp_prompt in mcp_prompts:
234
+ # Convert arguments if they exist
235
+ arguments = None
236
+ if hasattr(mcp_prompt, "arguments") and mcp_prompt.arguments:
237
+ arguments = [arg.model_dump() for arg in mcp_prompt.arguments]
238
+
239
+ prompt_infos.append(
240
+ PromptInfo(
241
+ key=mcp_prompt.name, # For 1.x, key and name are the same
242
+ name=mcp_prompt.name,
243
+ description=mcp_prompt.description,
244
+ arguments=arguments,
245
+ tags=None, # 1.x doesn't have tags
246
+ enabled=None, # 1.x doesn't have enabled field
247
+ )
248
+ )
249
+
250
+ # Extract detailed resource information from MCP Resource objects
251
+ resource_infos = []
252
+ for mcp_resource in mcp_resources:
253
+ resource_infos.append(
254
+ ResourceInfo(
255
+ key=str(mcp_resource.uri), # For 1.x, key and uri are the same
256
+ uri=str(mcp_resource.uri),
257
+ name=mcp_resource.name,
258
+ description=mcp_resource.description,
259
+ mime_type=mcp_resource.mimeType,
260
+ tags=None, # 1.x doesn't have tags
261
+ enabled=None, # 1.x doesn't have enabled field
262
+ )
263
+ )
264
+
265
+ # Extract detailed template information from MCP ResourceTemplate objects
266
+ template_infos = []
267
+ for mcp_template in mcp_templates:
268
+ template_infos.append(
269
+ TemplateInfo(
270
+ key=str(
271
+ mcp_template.uriTemplate
272
+ ), # For 1.x, key and uriTemplate are the same
273
+ uri_template=str(mcp_template.uriTemplate),
274
+ name=mcp_template.name,
275
+ description=mcp_template.description,
276
+ mime_type=mcp_template.mimeType,
277
+ tags=None, # 1.x doesn't have tags
278
+ enabled=None, # 1.x doesn't have enabled field
279
+ )
280
+ )
281
+
282
+ # Basic MCP capabilities
283
+ capabilities = {
284
+ "tools": {"listChanged": True},
285
+ "resources": {"subscribe": False, "listChanged": False},
286
+ "prompts": {"listChanged": False},
287
+ "logging": {},
288
+ }
289
+
290
+ return FastMCPInfo(
291
+ name=mcp.name,
292
+ instructions=getattr(mcp, "instructions", None),
293
+ fastmcp_version=fastmcp.__version__, # Report current fastmcp version
294
+ mcp_version=importlib.metadata.version("mcp"),
295
+ server_version="1.0", # FastMCP 1.x version
296
+ tools=tool_infos,
297
+ prompts=prompt_infos,
298
+ resources=resource_infos,
299
+ templates=template_infos, # FastMCP1x does have templates
300
+ capabilities=capabilities,
301
+ )
302
+
303
+
304
+ def _is_fastmcp_v1(mcp: Any) -> bool:
305
+ """Check if the given instance is a FastMCP v1.x instance."""
306
+
307
+ # Check if it's an instance of FastMCP1x and not FastMCP2
308
+ return isinstance(mcp, FastMCP1x) and not isinstance(mcp, FastMCP)
309
+
310
+
311
+ async def inspect_fastmcp(mcp: FastMCP[Any] | Any) -> FastMCPInfo:
312
+ """Extract information from a FastMCP instance into a dataclass.
313
+
314
+ This function automatically detects whether the instance is FastMCP v1.x or v2.x
315
+ and uses the appropriate extraction method.
316
+
317
+ Args:
318
+ mcp: The FastMCP instance to inspect (v1.x or v2.x)
319
+
320
+ Returns:
321
+ FastMCPInfo dataclass containing the extracted information
322
+ """
323
+ if _is_fastmcp_v1(mcp):
324
+ return await inspect_fastmcp_v1(mcp)
325
+ else:
326
+ return await inspect_fastmcp_v2(mcp)