fastmcp 2.0.0__py3-none-any.whl → 2.1.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.
fastmcp/server/openapi.py CHANGED
@@ -12,7 +12,7 @@ from pydantic.networks import AnyUrl
12
12
 
13
13
  from fastmcp.resources import Resource, ResourceTemplate
14
14
  from fastmcp.server.server import FastMCP
15
- from fastmcp.tools.base import Tool
15
+ from fastmcp.tools.tool import Tool
16
16
  from fastmcp.utilities import openapi
17
17
  from fastmcp.utilities.func_metadata import func_metadata
18
18
  from fastmcp.utilities.logging import get_logger
@@ -115,6 +115,7 @@ class OpenAPITool(Tool):
115
115
  parameters: dict[str, Any],
116
116
  fn_metadata: Any,
117
117
  is_async: bool = True,
118
+ tags: set[str] = set(),
118
119
  ):
119
120
  super().__init__(
120
121
  name=name,
@@ -124,6 +125,7 @@ class OpenAPITool(Tool):
124
125
  fn_metadata=fn_metadata,
125
126
  is_async=is_async,
126
127
  context_kwarg="context", # Default context keyword argument
128
+ tags=tags,
127
129
  )
128
130
  self._client = client
129
131
  self._route = route
@@ -242,12 +244,14 @@ class OpenAPIResource(Resource):
242
244
  name: str,
243
245
  description: str,
244
246
  mime_type: str = "application/json",
247
+ tags: set[str] = set(),
245
248
  ):
246
249
  super().__init__(
247
250
  uri=AnyUrl(uri), # Convert string to AnyUrl
248
251
  name=name,
249
252
  description=description,
250
253
  mime_type=mime_type,
254
+ tags=tags,
251
255
  )
252
256
  self._client = client
253
257
  self._route = route
@@ -332,6 +336,7 @@ class OpenAPIResourceTemplate(ResourceTemplate):
332
336
  name: str,
333
337
  description: str,
334
338
  parameters: dict[str, Any],
339
+ tags: set[str] = set(),
335
340
  ):
336
341
  super().__init__(
337
342
  uri_template=uri_template,
@@ -339,6 +344,7 @@ class OpenAPIResourceTemplate(ResourceTemplate):
339
344
  description=description,
340
345
  fn=self._create_resource_fn,
341
346
  parameters=parameters,
347
+ tags=tags,
342
348
  )
343
349
  self._client = client
344
350
  self._route = route
@@ -405,6 +411,7 @@ class OpenAPIResourceTemplate(ResourceTemplate):
405
411
  description=self.description
406
412
  or f"Resource for {self._route.path}", # Provide default if None
407
413
  mime_type="application/json", # Default, will be updated when read
414
+ tags=set(self._route.tags or []),
408
415
  )
409
416
 
410
417
 
@@ -525,10 +532,13 @@ class FastMCPOpenAPI(FastMCP):
525
532
  parameters=combined_schema,
526
533
  fn_metadata=func_metadata(_openapi_passthrough),
527
534
  is_async=True,
535
+ tags=set(route.tags or []),
528
536
  )
529
537
  # Register the tool by directly assigning to the tools dictionary
530
538
  self._tool_manager._tools[tool_name] = tool
531
- logger.debug(f"Registered TOOL: {tool_name} ({route.method} {route.path})")
539
+ logger.debug(
540
+ f"Registered TOOL: {tool_name} ({route.method} {route.path}) with tags: {route.tags}"
541
+ )
532
542
 
533
543
  def _create_openapi_resource(self, route: openapi.HTTPRoute, operation_id: str):
534
544
  """Creates and registers an OpenAPIResource with enhanced description."""
@@ -550,11 +560,12 @@ class FastMCPOpenAPI(FastMCP):
550
560
  uri=resource_uri,
551
561
  name=resource_name,
552
562
  description=enhanced_description,
563
+ tags=set(route.tags or []),
553
564
  )
554
565
  # Register the resource by directly assigning to the resources dictionary
555
566
  self._resource_manager._resources[str(resource.uri)] = resource
556
567
  logger.debug(
557
- f"Registered RESOURCE: {resource_uri} ({route.method} {route.path})"
568
+ f"Registered RESOURCE: {resource_uri} ({route.method} {route.path}) with tags: {route.tags}"
558
569
  )
559
570
 
560
571
  def _create_openapi_template(self, route: openapi.HTTPRoute, operation_id: str):
@@ -594,11 +605,12 @@ class FastMCPOpenAPI(FastMCP):
594
605
  name=template_name,
595
606
  description=enhanced_description,
596
607
  parameters=template_params_schema,
608
+ tags=set(route.tags or []),
597
609
  )
598
610
  # Register the template by directly assigning to the templates dictionary
599
611
  self._resource_manager._templates[uri_template_str] = template
600
612
  logger.debug(
601
- f"Registered TEMPLATE: {uri_template_str} ({route.method} {route.path})"
613
+ f"Registered TEMPLATE: {uri_template_str} ({route.method} {route.path}) with tags: {route.tags}"
602
614
  )
603
615
 
604
616
  async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
fastmcp/server/proxy.py CHANGED
@@ -9,7 +9,7 @@ from fastmcp.prompts import Prompt
9
9
  from fastmcp.resources import Resource, ResourceTemplate
10
10
  from fastmcp.server.context import Context
11
11
  from fastmcp.server.server import FastMCP
12
- from fastmcp.tools.base import Tool
12
+ from fastmcp.tools.tool import Tool
13
13
  from fastmcp.utilities.func_metadata import func_metadata
14
14
  from fastmcp.utilities.logging import get_logger
15
15
 
@@ -40,11 +40,15 @@ class ProxyTool(Tool):
40
40
  async def run(
41
41
  self, arguments: dict[str, Any], context: Context | None = None
42
42
  ) -> Any:
43
+ # the client context manager will swallow any exceptions inside a TaskGroup
44
+ # so we return the raw result and raise an exception ourselves
43
45
  async with self._client:
44
- result = await self._client.call_tool(self.name, arguments)
46
+ result = await self._client.call_tool(
47
+ self.name, arguments, _return_raw_result=True
48
+ )
45
49
  if result.isError:
46
50
  raise ValueError(cast(mcp.types.TextContent, result.content[0]).text)
47
- return result.content[0]
51
+ return result.content
48
52
 
49
53
 
50
54
  class ProxyResource(Resource):
@@ -73,12 +77,12 @@ class ProxyResource(Resource):
73
77
 
74
78
  async with self._client:
75
79
  result = await self._client.read_resource(self.uri)
76
- if isinstance(result.contents[0], TextResourceContents):
77
- return result.contents[0].text
78
- elif isinstance(result.contents[0], BlobResourceContents):
79
- return result.contents[0].blob
80
+ if isinstance(result[0], TextResourceContents):
81
+ return result[0].text
82
+ elif isinstance(result[0], BlobResourceContents):
83
+ return result[0].blob
80
84
  else:
81
- raise ValueError(f"Unsupported content type: {type(result.contents[0])}")
85
+ raise ValueError(f"Unsupported content type: {type(result[0])}")
82
86
 
83
87
 
84
88
  class ProxyTemplate(ResourceTemplate):
@@ -103,20 +107,20 @@ class ProxyTemplate(ResourceTemplate):
103
107
  async with self._client:
104
108
  result = await self._client.read_resource(uri)
105
109
 
106
- if isinstance(result.contents[0], TextResourceContents):
107
- value = result.contents[0].text
108
- elif isinstance(result.contents[0], BlobResourceContents):
109
- value = result.contents[0].blob
110
+ if isinstance(result[0], TextResourceContents):
111
+ value = result[0].text
112
+ elif isinstance(result[0], BlobResourceContents):
113
+ value = result[0].blob
110
114
  else:
111
- raise ValueError(f"Unsupported content type: {type(result.contents[0])}")
115
+ raise ValueError(f"Unsupported content type: {type(result[0])}")
112
116
 
113
117
  return ProxyResource(
114
118
  client=self._client,
115
119
  uri=uri,
116
120
  name=self.name,
117
121
  description=self.description,
118
- mime_type=result.contents[0].mimeType,
119
- contents=result.contents,
122
+ mime_type=result[0].mimeType,
123
+ contents=result,
120
124
  _value=value,
121
125
  )
122
126
 
@@ -177,15 +181,15 @@ class FastMCPProxy(FastMCP):
177
181
 
178
182
  async with client:
179
183
  # Register proxies for client tools
180
- tools_result = await client.list_tools()
181
- for tool in tools_result.tools:
184
+ tools = await client.list_tools()
185
+ for tool in tools:
182
186
  tool_proxy = await ProxyTool.from_client(client, tool)
183
187
  server._tool_manager._tools[tool_proxy.name] = tool_proxy
184
188
  logger.debug(f"Created proxy for tool: {tool_proxy.name}")
185
189
 
186
190
  # Register proxies for client resources
187
- resources_result = await client.list_resources()
188
- for resource in resources_result.resources:
191
+ resources = await client.list_resources()
192
+ for resource in resources:
189
193
  resource_proxy = await ProxyResource.from_client(client, resource)
190
194
  server._resource_manager._resources[str(resource_proxy.uri)] = (
191
195
  resource_proxy
@@ -193,8 +197,8 @@ class FastMCPProxy(FastMCP):
193
197
  logger.debug(f"Created proxy for resource: {resource_proxy.uri}")
194
198
 
195
199
  # Register proxies for client resource templates
196
- templates_result = await client.list_resource_templates()
197
- for template in templates_result.resourceTemplates:
200
+ templates = await client.list_resource_templates()
201
+ for template in templates:
198
202
  template_proxy = await ProxyTemplate.from_client(client, template)
199
203
  server._resource_manager._templates[template_proxy.uri_template] = (
200
204
  template_proxy
@@ -204,8 +208,8 @@ class FastMCPProxy(FastMCP):
204
208
  )
205
209
 
206
210
  # Register proxies for client prompts
207
- prompts_result = await client.list_prompts()
208
- for prompt in prompts_result.prompts:
211
+ prompts = await client.list_prompts()
212
+ for prompt in prompts:
209
213
  prompt_proxy = await ProxyPrompt.from_client(client, prompt)
210
214
  server._prompt_manager._prompts[prompt_proxy.name] = prompt_proxy
211
215
  logger.debug(f"Created proxy for prompt: {prompt_proxy.name}")
fastmcp/server/server.py CHANGED
@@ -3,9 +3,10 @@
3
3
  import inspect
4
4
  import json
5
5
  import re
6
- from collections.abc import AsyncIterator, Callable, Sequence
6
+ from collections.abc import AsyncIterator, Callable
7
7
  from contextlib import (
8
8
  AbstractAsyncContextManager,
9
+ AsyncExitStack,
9
10
  asynccontextmanager,
10
11
  )
11
12
  from typing import TYPE_CHECKING, Any, Generic, Literal
@@ -18,7 +19,6 @@ from fastapi import FastAPI
18
19
  from mcp.server.lowlevel.helper_types import ReadResourceContents
19
20
  from mcp.server.lowlevel.server import LifespanResultT
20
21
  from mcp.server.lowlevel.server import Server as MCPServer
21
- from mcp.server.lowlevel.server import lifespan as default_lifespan
22
22
  from mcp.server.session import ServerSession
23
23
  from mcp.server.sse import SseServerTransport
24
24
  from mcp.server.stdio import stdio_server
@@ -56,6 +56,19 @@ if TYPE_CHECKING:
56
56
  logger = get_logger(__name__)
57
57
 
58
58
 
59
+ @asynccontextmanager
60
+ async def default_lifespan(server: "FastMCP") -> AsyncIterator[Any]:
61
+ """Default lifespan context manager that does nothing.
62
+
63
+ Args:
64
+ server: The server instance this lifespan is managing
65
+
66
+ Returns:
67
+ An empty context object
68
+ """
69
+ yield {}
70
+
71
+
59
72
  def lifespan_wrapper(
60
73
  app: "FastMCP",
61
74
  lifespan: Callable[["FastMCP"], AbstractAsyncContextManager[LifespanResultT]],
@@ -64,7 +77,18 @@ def lifespan_wrapper(
64
77
  ]:
65
78
  @asynccontextmanager
66
79
  async def wrap(s: MCPServer[LifespanResultT]) -> AsyncIterator[LifespanResultT]:
67
- async with lifespan(app) as context:
80
+ async with AsyncExitStack() as stack:
81
+ # enter main app's lifespan
82
+ context = await stack.enter_async_context(lifespan(app))
83
+
84
+ # Enter all mounted app lifespans
85
+ for prefix, mounted_app in app._mounted_apps.items():
86
+ mounted_context = mounted_app._mcp_server.lifespan(
87
+ mounted_app._mcp_server
88
+ )
89
+ await stack.enter_async_context(mounted_context)
90
+ logger.debug(f"Prepared lifespan for mounted app '{prefix}'")
91
+
68
92
  yield context
69
93
 
70
94
  return wrap
@@ -78,29 +102,34 @@ class FastMCP(Generic[LifespanResultT]):
78
102
  lifespan: (
79
103
  Callable[["FastMCP"], AbstractAsyncContextManager[LifespanResultT]] | None
80
104
  ) = None,
105
+ tags: set[str] | None = None,
81
106
  **settings: Any,
82
107
  ):
108
+ self.tags: set[str] = tags or set()
83
109
  self.settings = fastmcp.settings.ServerSettings(**settings)
84
110
 
111
+ # Setup for mounted apps - must be initialized before _mcp_server
112
+ self._mounted_apps: dict[str, FastMCP] = {}
113
+
114
+ if lifespan is None:
115
+ lifespan = default_lifespan
116
+
85
117
  self._mcp_server = MCPServer[LifespanResultT](
86
118
  name=name or "FastMCP",
87
119
  instructions=instructions,
88
- lifespan=lifespan_wrapper(self, lifespan) if lifespan else default_lifespan, # type: ignore
120
+ lifespan=lifespan_wrapper(self, lifespan),
89
121
  )
90
122
  self._tool_manager = ToolManager(
91
- warn_on_duplicate_tools=self.settings.warn_on_duplicate_tools
123
+ duplicate_behavior=self.settings.on_duplicate_tools
92
124
  )
93
125
  self._resource_manager = ResourceManager(
94
- warn_on_duplicate_resources=self.settings.warn_on_duplicate_resources
126
+ duplicate_behavior=self.settings.on_duplicate_resources
95
127
  )
96
128
  self._prompt_manager = PromptManager(
97
- warn_on_duplicate_prompts=self.settings.warn_on_duplicate_prompts
129
+ duplicate_behavior=self.settings.on_duplicate_prompts
98
130
  )
99
131
  self.dependencies = self.settings.dependencies
100
132
 
101
- # Setup for mounted apps
102
- self._mounted_apps: dict[str, FastMCP] = {}
103
-
104
133
  # Set up MCP protocol handlers
105
134
  self._setup_handlers()
106
135
 
@@ -151,6 +180,7 @@ class FastMCP(Generic[LifespanResultT]):
151
180
 
152
181
  async def list_tools(self) -> list[MCPTool]:
153
182
  """List all available tools."""
183
+
154
184
  tools = self._tool_manager.list_tools()
155
185
  return [
156
186
  MCPTool(
@@ -177,7 +207,7 @@ class FastMCP(Generic[LifespanResultT]):
177
207
 
178
208
  async def call_tool(
179
209
  self, name: str, arguments: dict[str, Any]
180
- ) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
210
+ ) -> list[TextContent | ImageContent | EmbeddedResource]:
181
211
  """Call a tool by name with arguments."""
182
212
  context = self.get_context()
183
213
  result = await self._tool_manager.call_tool(name, arguments, context=context)
@@ -228,6 +258,7 @@ class FastMCP(Generic[LifespanResultT]):
228
258
  fn: AnyFunction,
229
259
  name: str | None = None,
230
260
  description: str | None = None,
261
+ tags: set[str] | None = None,
231
262
  ) -> None:
232
263
  """Add a tool to the server.
233
264
 
@@ -238,11 +269,17 @@ class FastMCP(Generic[LifespanResultT]):
238
269
  fn: The function to register as a tool
239
270
  name: Optional name for the tool (defaults to function name)
240
271
  description: Optional description of what the tool does
272
+ tags: Optional set of tags for categorizing the tool
241
273
  """
242
- self._tool_manager.add_tool(fn, name=name, description=description)
274
+ self._tool_manager.add_tool_from_fn(
275
+ fn, name=name, description=description, tags=tags
276
+ )
243
277
 
244
278
  def tool(
245
- self, name: str | None = None, description: str | None = None
279
+ self,
280
+ name: str | None = None,
281
+ description: str | None = None,
282
+ tags: set[str] | None = None,
246
283
  ) -> Callable[[AnyFunction], AnyFunction]:
247
284
  """Decorator to register a tool.
248
285
 
@@ -253,6 +290,7 @@ class FastMCP(Generic[LifespanResultT]):
253
290
  Args:
254
291
  name: Optional name for the tool (defaults to function name)
255
292
  description: Optional description of what the tool does
293
+ tags: Optional set of tags for categorizing the tool
256
294
 
257
295
  Example:
258
296
  @server.tool()
@@ -277,7 +315,7 @@ class FastMCP(Generic[LifespanResultT]):
277
315
  )
278
316
 
279
317
  def decorator(fn: AnyFunction) -> AnyFunction:
280
- self.add_tool(fn, name=name, description=description)
318
+ self.add_tool(fn, name=name, description=description, tags=tags)
281
319
  return fn
282
320
 
283
321
  return decorator
@@ -297,6 +335,7 @@ class FastMCP(Generic[LifespanResultT]):
297
335
  name: str | None = None,
298
336
  description: str | None = None,
299
337
  mime_type: str | None = None,
338
+ tags: set[str] | None = None,
300
339
  ) -> Callable[[AnyFunction], AnyFunction]:
301
340
  """Decorator to register a function as a resource.
302
341
 
@@ -314,6 +353,7 @@ class FastMCP(Generic[LifespanResultT]):
314
353
  name: Optional name for the resource
315
354
  description: Optional description of the resource
316
355
  mime_type: Optional MIME type for the resource
356
+ tags: Optional set of tags for categorizing the resource
317
357
 
318
358
  Example:
319
359
  @server.resource("resource://my-resource")
@@ -358,12 +398,13 @@ class FastMCP(Generic[LifespanResultT]):
358
398
  )
359
399
 
360
400
  # Register as template
361
- self._resource_manager.add_template(
401
+ self._resource_manager.add_template_from_fn(
362
402
  fn=fn,
363
403
  uri_template=uri,
364
404
  name=name,
365
405
  description=description,
366
406
  mime_type=mime_type or "text/plain",
407
+ tags=tags,
367
408
  )
368
409
  else:
369
410
  # Register as regular resource
@@ -373,6 +414,7 @@ class FastMCP(Generic[LifespanResultT]):
373
414
  description=description,
374
415
  mime_type=mime_type or "text/plain",
375
416
  fn=fn,
417
+ tags=tags or set(), # Default to empty set if None
376
418
  )
377
419
  self.add_resource(resource)
378
420
  return fn
@@ -388,13 +430,17 @@ class FastMCP(Generic[LifespanResultT]):
388
430
  self._prompt_manager.add_prompt(prompt)
389
431
 
390
432
  def prompt(
391
- self, name: str | None = None, description: str | None = None
433
+ self,
434
+ name: str | None = None,
435
+ description: str | None = None,
436
+ tags: set[str] | None = None,
392
437
  ) -> Callable[[AnyFunction], AnyFunction]:
393
438
  """Decorator to register a prompt.
394
439
 
395
440
  Args:
396
441
  name: Optional name for the prompt (defaults to function name)
397
442
  description: Optional description of what the prompt does
443
+ tags: Optional set of tags for categorizing the prompt
398
444
 
399
445
  Example:
400
446
  @server.prompt()
@@ -431,7 +477,9 @@ class FastMCP(Generic[LifespanResultT]):
431
477
  )
432
478
 
433
479
  def decorator(func: AnyFunction) -> AnyFunction:
434
- prompt = Prompt.from_function(func, name=name, description=description)
480
+ prompt = Prompt.from_function(
481
+ func, name=name, description=description, tags=tags
482
+ )
435
483
  self.add_prompt(prompt)
436
484
  return func
437
485
 
@@ -514,37 +562,56 @@ class FastMCP(Generic[LifespanResultT]):
514
562
  logger.error(f"Error getting prompt {name}: {e}")
515
563
  raise ValueError(str(e))
516
564
 
517
- def mount(self, prefix: str, app: "FastMCP") -> None:
565
+ def mount(
566
+ self,
567
+ prefix: str,
568
+ app: "FastMCP",
569
+ tool_separator: str | None = None,
570
+ resource_separator: str | None = None,
571
+ prompt_separator: str | None = None,
572
+ ) -> None:
518
573
  """Mount another FastMCP application with a given prefix.
519
574
 
520
575
  When an application is mounted:
521
- - The tools are imported with prefixed names
522
- Example: If app has a tool named "get_weather", it will be available as "weather/get_weather"
523
- - The resources are imported with prefixed URIs
576
+ - The tools are imported with prefixed names using the tool_separator
577
+ Example: If app has a tool named "get_weather", it will be available as "weatherget_weather"
578
+ - The resources are imported with prefixed URIs using the resource_separator
524
579
  Example: If app has a resource with URI "weather://forecast", it will be available as "weather+weather://forecast"
525
- - The templates are imported with prefixed URI templates
580
+ - The templates are imported with prefixed URI templates using the resource_separator
526
581
  Example: If app has a template with URI "weather://location/{id}", it will be available as "weather+weather://location/{id}"
527
- - The prompts are imported with prefixed names
528
- Example: If app has a prompt named "weather_prompt", it will be available as "weather/weather_prompt"
582
+ - The prompts are imported with prefixed names using the prompt_separator
583
+ Example: If app has a prompt named "weather_prompt", it will be available as "weather_weather_prompt"
584
+ - The mounted app's lifespan will be executed when the parent app's lifespan runs,
585
+ ensuring that any setup needed by the mounted app is performed
529
586
 
530
587
  Args:
531
588
  prefix: The prefix to use for the mounted application
532
589
  app: The FastMCP application to mount
590
+ tool_separator: Separator for tool names (defaults to "_")
591
+ resource_separator: Separator for resource URIs (defaults to "+")
592
+ prompt_separator: Separator for prompt names (defaults to "_")
533
593
  """
594
+ if tool_separator is None:
595
+ tool_separator = "_"
596
+ if resource_separator is None:
597
+ resource_separator = "+"
598
+ if prompt_separator is None:
599
+ prompt_separator = "_"
600
+
534
601
  # Mount the app in the list of mounted apps
535
602
  self._mounted_apps[prefix] = app
536
603
 
537
- # Import tools from the mounted app with / delimiter
538
- tool_prefix = f"{prefix}/"
604
+ # Import tools from the mounted app
605
+ tool_prefix = f"{prefix}{tool_separator}"
539
606
  self._tool_manager.import_tools(app._tool_manager, tool_prefix)
540
607
 
541
- # Import resources and templates from the mounted app with + delimiter
542
- resource_prefix = f"{prefix}+"
608
+ # Import resources and templates from the mounted app
609
+ resource_prefix = f"{prefix}{resource_separator}"
543
610
  self._resource_manager.import_resources(app._resource_manager, resource_prefix)
544
611
  self._resource_manager.import_templates(app._resource_manager, resource_prefix)
545
612
 
546
- # Import prompts with / delimiter
547
- prompt_prefix = f"{prefix}/"
613
+ # Import prompts from the mounted app
614
+ prompt_prefix = f"{prefix}{prompt_separator}"
548
615
  self._prompt_manager.import_prompts(app._prompt_manager, prompt_prefix)
549
616
 
550
617
  logger.info(f"Mounted app with prefix '{prefix}'")
fastmcp/settings.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations as _annotations
2
2
 
3
+ from enum import Enum
3
4
  from typing import TYPE_CHECKING, Literal
4
5
 
5
6
  from pydantic import Field
@@ -11,6 +12,13 @@ if TYPE_CHECKING:
11
12
  LOG_LEVEL = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
12
13
 
13
14
 
15
+ class DuplicateBehavior(Enum):
16
+ WARN = "warn"
17
+ ERROR = "error"
18
+ REPLACE = "replace"
19
+ IGNORE = "ignore"
20
+
21
+
14
22
  class Settings(BaseSettings):
15
23
  """FastMCP settings."""
16
24
 
@@ -47,13 +55,13 @@ class ServerSettings(BaseSettings):
47
55
  debug: bool = False
48
56
 
49
57
  # resource settings
50
- warn_on_duplicate_resources: bool = True
58
+ on_duplicate_resources: DuplicateBehavior = DuplicateBehavior.WARN
51
59
 
52
60
  # tool settings
53
- warn_on_duplicate_tools: bool = True
61
+ on_duplicate_tools: DuplicateBehavior = DuplicateBehavior.WARN
54
62
 
55
63
  # prompt settings
56
- warn_on_duplicate_prompts: bool = True
64
+ on_duplicate_prompts: DuplicateBehavior = DuplicateBehavior.WARN
57
65
 
58
66
  dependencies: list[str] = Field(
59
67
  default_factory=list,
fastmcp/tools/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- from .base import Tool
1
+ from .tool import Tool
2
2
  from .tool_manager import ToolManager
3
3
 
4
4
  __all__ = ["Tool", "ToolManager"]
@@ -2,12 +2,14 @@ from __future__ import annotations as _annotations
2
2
 
3
3
  import inspect
4
4
  from collections.abc import Callable
5
- from typing import TYPE_CHECKING, Any
5
+ from typing import TYPE_CHECKING, Annotated, Any
6
6
 
7
- from pydantic import BaseModel, Field
7
+ from pydantic import BaseModel, BeforeValidator, Field
8
+ from typing_extensions import Self
8
9
 
9
10
  from fastmcp.exceptions import ToolError
10
11
  from fastmcp.utilities.func_metadata import FuncMetadata, func_metadata
12
+ from fastmcp.utilities.types import _convert_set_defaults
11
13
 
12
14
  if TYPE_CHECKING:
13
15
  from mcp.server.session import ServerSessionT
@@ -19,7 +21,7 @@ if TYPE_CHECKING:
19
21
  class Tool(BaseModel):
20
22
  """Internal tool registration info."""
21
23
 
22
- fn: Callable[..., Any] = Field(exclude=True)
24
+ fn: Callable[..., Any]
23
25
  name: str = Field(description="Name of the tool")
24
26
  description: str = Field(description="Description of what the tool does")
25
27
  parameters: dict[str, Any] = Field(description="JSON schema for tool parameters")
@@ -31,6 +33,9 @@ class Tool(BaseModel):
31
33
  context_kwarg: str | None = Field(
32
34
  None, description="Name of the kwarg that should receive context"
33
35
  )
36
+ tags: Annotated[set[str], BeforeValidator(_convert_set_defaults)] = Field(
37
+ default_factory=set, description="Tags for the tool"
38
+ )
34
39
 
35
40
  @classmethod
36
41
  def from_function(
@@ -39,6 +44,7 @@ class Tool(BaseModel):
39
44
  name: str | None = None,
40
45
  description: str | None = None,
41
46
  context_kwarg: str | None = None,
47
+ tags: set[str] | None = None,
42
48
  ) -> Tool:
43
49
  """Create a Tool from a function."""
44
50
  from fastmcp import Context
@@ -72,6 +78,7 @@ class Tool(BaseModel):
72
78
  fn_metadata=func_arg_metadata,
73
79
  is_async=is_async,
74
80
  context_kwarg=context_kwarg,
81
+ tags=tags or set(),
75
82
  )
76
83
 
77
84
  async def run(
@@ -91,3 +98,15 @@ class Tool(BaseModel):
91
98
  )
92
99
  except Exception as e:
93
100
  raise ToolError(f"Error executing tool {self.name}: {e}") from e
101
+
102
+ def copy(self, updates: dict[str, Any] | None = None) -> Self:
103
+ """Copy the tool with optional updates."""
104
+ data = self.model_dump()
105
+ if updates:
106
+ data.update(updates)
107
+ return type(self)(**data)
108
+
109
+ def __eq__(self, other: object) -> bool:
110
+ if not isinstance(other, Tool):
111
+ return False
112
+ return self.model_dump() == other.model_dump()