fastmcp 2.0.0__py3-none-any.whl → 2.1.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.
fastmcp/server/server.py CHANGED
@@ -1,11 +1,10 @@
1
1
  """FastMCP - A more ergonomic interface for MCP servers."""
2
2
 
3
- import inspect
4
3
  import json
5
- import re
6
- from collections.abc import AsyncIterator, Callable, Sequence
4
+ from collections.abc import AsyncIterator, Awaitable, Callable
7
5
  from contextlib import (
8
6
  AbstractAsyncContextManager,
7
+ AsyncExitStack,
9
8
  asynccontextmanager,
10
9
  )
11
10
  from typing import TYPE_CHECKING, Any, Generic, Literal
@@ -18,7 +17,6 @@ from fastapi import FastAPI
18
17
  from mcp.server.lowlevel.helper_types import ReadResourceContents
19
18
  from mcp.server.lowlevel.server import LifespanResultT
20
19
  from mcp.server.lowlevel.server import Server as MCPServer
21
- from mcp.server.lowlevel.server import lifespan as default_lifespan
22
20
  from mcp.server.session import ServerSession
23
21
  from mcp.server.sse import SseServerTransport
24
22
  from mcp.server.stdio import stdio_server
@@ -43,8 +41,12 @@ import fastmcp
43
41
  import fastmcp.settings
44
42
  from fastmcp.exceptions import ResourceError
45
43
  from fastmcp.prompts import Prompt, PromptManager
46
- from fastmcp.resources import FunctionResource, Resource, ResourceManager
44
+ from fastmcp.prompts.prompt import Message, PromptResult
45
+ from fastmcp.resources import Resource, ResourceManager
46
+ from fastmcp.resources.template import ResourceTemplate
47
47
  from fastmcp.tools import ToolManager
48
+ from fastmcp.tools.tool import Tool
49
+ from fastmcp.utilities.decorators import DecoratedFunction
48
50
  from fastmcp.utilities.logging import configure_logging, get_logger
49
51
  from fastmcp.utilities.types import Image
50
52
 
@@ -56,6 +58,19 @@ if TYPE_CHECKING:
56
58
  logger = get_logger(__name__)
57
59
 
58
60
 
61
+ @asynccontextmanager
62
+ async def default_lifespan(server: "FastMCP") -> AsyncIterator[Any]:
63
+ """Default lifespan context manager that does nothing.
64
+
65
+ Args:
66
+ server: The server instance this lifespan is managing
67
+
68
+ Returns:
69
+ An empty context object
70
+ """
71
+ yield {}
72
+
73
+
59
74
  def lifespan_wrapper(
60
75
  app: "FastMCP",
61
76
  lifespan: Callable[["FastMCP"], AbstractAsyncContextManager[LifespanResultT]],
@@ -64,7 +79,18 @@ def lifespan_wrapper(
64
79
  ]:
65
80
  @asynccontextmanager
66
81
  async def wrap(s: MCPServer[LifespanResultT]) -> AsyncIterator[LifespanResultT]:
67
- async with lifespan(app) as context:
82
+ async with AsyncExitStack() as stack:
83
+ # enter main app's lifespan
84
+ context = await stack.enter_async_context(lifespan(app))
85
+
86
+ # Enter all mounted app lifespans
87
+ for prefix, mounted_app in app._mounted_apps.items():
88
+ mounted_context = mounted_app._mcp_server.lifespan(
89
+ mounted_app._mcp_server
90
+ )
91
+ await stack.enter_async_context(mounted_context)
92
+ logger.debug(f"Prepared lifespan for mounted app '{prefix}'")
93
+
68
94
  yield context
69
95
 
70
96
  return wrap
@@ -78,29 +104,34 @@ class FastMCP(Generic[LifespanResultT]):
78
104
  lifespan: (
79
105
  Callable[["FastMCP"], AbstractAsyncContextManager[LifespanResultT]] | None
80
106
  ) = None,
107
+ tags: set[str] | None = None,
81
108
  **settings: Any,
82
109
  ):
110
+ self.tags: set[str] = tags or set()
83
111
  self.settings = fastmcp.settings.ServerSettings(**settings)
84
112
 
113
+ # Setup for mounted apps - must be initialized before _mcp_server
114
+ self._mounted_apps: dict[str, FastMCP] = {}
115
+
116
+ if lifespan is None:
117
+ lifespan = default_lifespan
118
+
85
119
  self._mcp_server = MCPServer[LifespanResultT](
86
120
  name=name or "FastMCP",
87
121
  instructions=instructions,
88
- lifespan=lifespan_wrapper(self, lifespan) if lifespan else default_lifespan, # type: ignore
122
+ lifespan=lifespan_wrapper(self, lifespan),
89
123
  )
90
124
  self._tool_manager = ToolManager(
91
- warn_on_duplicate_tools=self.settings.warn_on_duplicate_tools
125
+ duplicate_behavior=self.settings.on_duplicate_tools
92
126
  )
93
127
  self._resource_manager = ResourceManager(
94
- warn_on_duplicate_resources=self.settings.warn_on_duplicate_resources
128
+ duplicate_behavior=self.settings.on_duplicate_resources
95
129
  )
96
130
  self._prompt_manager = PromptManager(
97
- warn_on_duplicate_prompts=self.settings.warn_on_duplicate_prompts
131
+ duplicate_behavior=self.settings.on_duplicate_prompts
98
132
  )
99
133
  self.dependencies = self.settings.dependencies
100
134
 
101
- # Setup for mounted apps
102
- self._mounted_apps: dict[str, FastMCP] = {}
103
-
104
135
  # Set up MCP protocol handlers
105
136
  self._setup_handlers()
106
137
 
@@ -137,21 +168,32 @@ class FastMCP(Generic[LifespanResultT]):
137
168
  Args:
138
169
  transport: Transport protocol to use ("stdio" or "sse")
139
170
  """
171
+ logger.info(f'Starting server "{self.name}"...')
140
172
  anyio.run(self.run_async, transport)
141
173
 
142
174
  def _setup_handlers(self) -> None:
143
175
  """Set up core MCP protocol handlers."""
144
- self._mcp_server.list_tools()(self.list_tools)
176
+ self._mcp_server.list_tools()(self._mcp_list_tools)
145
177
  self._mcp_server.call_tool()(self.call_tool)
146
- self._mcp_server.list_resources()(self.list_resources)
147
- self._mcp_server.read_resource()(self.read_resource)
148
- self._mcp_server.list_prompts()(self.list_prompts)
149
- self._mcp_server.get_prompt()(self.get_prompt)
150
- self._mcp_server.list_resource_templates()(self.list_resource_templates)
151
-
152
- async def list_tools(self) -> list[MCPTool]:
153
- """List all available tools."""
154
- tools = self._tool_manager.list_tools()
178
+ self._mcp_server.list_resources()(self._mcp_list_resources)
179
+ self._mcp_server.read_resource()(self._mcp_read_resource)
180
+ self._mcp_server.list_prompts()(self._mcp_list_prompts)
181
+ self._mcp_server.get_prompt()(self._mcp_get_prompt)
182
+ self._mcp_server.list_resource_templates()(self._mcp_list_resource_templates)
183
+
184
+ def list_tools(self) -> list[Tool]:
185
+ return self._tool_manager.list_tools()
186
+
187
+ async def _mcp_list_tools(self) -> list[MCPTool]:
188
+ """
189
+ List all available tools, in the format expected by the low-level MCP
190
+ server.
191
+
192
+ See `list_tools` for a more ergonomic way to list tools.
193
+ """
194
+
195
+ tools = self.list_tools()
196
+
155
197
  return [
156
198
  MCPTool(
157
199
  name=info.name,
@@ -177,17 +219,25 @@ class FastMCP(Generic[LifespanResultT]):
177
219
 
178
220
  async def call_tool(
179
221
  self, name: str, arguments: dict[str, Any]
180
- ) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
222
+ ) -> list[TextContent | ImageContent | EmbeddedResource]:
181
223
  """Call a tool by name with arguments."""
182
224
  context = self.get_context()
183
225
  result = await self._tool_manager.call_tool(name, arguments, context=context)
184
226
  converted_result = _convert_to_content(result)
185
227
  return converted_result
186
228
 
187
- async def list_resources(self) -> list[MCPResource]:
188
- """List all available resources."""
229
+ def list_resources(self) -> list[Resource]:
230
+ return self._resource_manager.list_resources()
189
231
 
190
- resources = self._resource_manager.list_resources()
232
+ async def _mcp_list_resources(self) -> list[MCPResource]:
233
+ """
234
+ List all available resources, in the format expected by the low-level MCP
235
+ server.
236
+
237
+ See `list_resources` for a more ergonomic way to list resources.
238
+ """
239
+
240
+ resources = self.list_resources()
191
241
  return [
192
242
  MCPResource(
193
243
  uri=resource.uri,
@@ -198,8 +248,18 @@ class FastMCP(Generic[LifespanResultT]):
198
248
  for resource in resources
199
249
  ]
200
250
 
201
- async def list_resource_templates(self) -> list[MCPResourceTemplate]:
202
- templates = self._resource_manager.list_templates()
251
+ def list_resource_templates(self) -> list[ResourceTemplate]:
252
+ return self._resource_manager.list_templates()
253
+
254
+ async def _mcp_list_resource_templates(self) -> list[MCPResourceTemplate]:
255
+ """
256
+ List all available resource templates, in the format expected by the low-level
257
+ MCP server.
258
+
259
+ See `list_resource_templates` for a more ergonomic way to list resource
260
+ templates.
261
+ """
262
+ templates = self.list_resource_templates()
203
263
  return [
204
264
  MCPResourceTemplate(
205
265
  uriTemplate=template.uri_template,
@@ -209,15 +269,27 @@ class FastMCP(Generic[LifespanResultT]):
209
269
  for template in templates
210
270
  ]
211
271
 
212
- async def read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
272
+ async def read_resource(self, uri: AnyUrl | str) -> str | bytes:
213
273
  """Read a resource by URI."""
274
+ resource = await self._resource_manager.get_resource(uri)
275
+ if not resource:
276
+ raise ResourceError(f"Unknown resource: {uri}")
277
+ return await resource.read()
278
+
279
+ async def _mcp_read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
280
+ """
281
+ Read a resource by URI, in the format expected by the low-level MCP
282
+ server.
283
+
284
+ See `read_resource` for a more ergonomic way to read resources.
285
+ """
214
286
 
215
287
  resource = await self._resource_manager.get_resource(uri)
216
288
  if not resource:
217
289
  raise ResourceError(f"Unknown resource: {uri}")
218
290
 
219
291
  try:
220
- content = await resource.read()
292
+ content = await self.read_resource(uri)
221
293
  return [ReadResourceContents(content=content, mime_type=resource.mime_type)]
222
294
  except Exception as e:
223
295
  logger.error(f"Error reading resource {uri}: {e}")
@@ -228,6 +300,7 @@ class FastMCP(Generic[LifespanResultT]):
228
300
  fn: AnyFunction,
229
301
  name: str | None = None,
230
302
  description: str | None = None,
303
+ tags: set[str] | None = None,
231
304
  ) -> None:
232
305
  """Add a tool to the server.
233
306
 
@@ -238,11 +311,17 @@ class FastMCP(Generic[LifespanResultT]):
238
311
  fn: The function to register as a tool
239
312
  name: Optional name for the tool (defaults to function name)
240
313
  description: Optional description of what the tool does
314
+ tags: Optional set of tags for categorizing the tool
241
315
  """
242
- self._tool_manager.add_tool(fn, name=name, description=description)
316
+ self._tool_manager.add_tool_from_fn(
317
+ fn, name=name, description=description, tags=tags
318
+ )
243
319
 
244
320
  def tool(
245
- self, name: str | None = None, description: str | None = None
321
+ self,
322
+ name: str | None = None,
323
+ description: str | None = None,
324
+ tags: set[str] | None = None,
246
325
  ) -> Callable[[AnyFunction], AnyFunction]:
247
326
  """Decorator to register a tool.
248
327
 
@@ -253,6 +332,7 @@ class FastMCP(Generic[LifespanResultT]):
253
332
  Args:
254
333
  name: Optional name for the tool (defaults to function name)
255
334
  description: Optional description of what the tool does
335
+ tags: Optional set of tags for categorizing the tool
256
336
 
257
337
  Example:
258
338
  @server.tool()
@@ -269,6 +349,7 @@ class FastMCP(Generic[LifespanResultT]):
269
349
  await context.report_progress(50, 100)
270
350
  return str(x)
271
351
  """
352
+
272
353
  # Check if user passed function directly instead of calling decorator
273
354
  if callable(name):
274
355
  raise TypeError(
@@ -277,8 +358,8 @@ class FastMCP(Generic[LifespanResultT]):
277
358
  )
278
359
 
279
360
  def decorator(fn: AnyFunction) -> AnyFunction:
280
- self.add_tool(fn, name=name, description=description)
281
- return fn
361
+ self.add_tool(fn, name=name, description=description, tags=tags)
362
+ return DecoratedFunction(fn)
282
363
 
283
364
  return decorator
284
365
 
@@ -288,8 +369,40 @@ class FastMCP(Generic[LifespanResultT]):
288
369
  Args:
289
370
  resource: A Resource instance to add
290
371
  """
372
+
291
373
  self._resource_manager.add_resource(resource)
292
374
 
375
+ def add_resource_fn(
376
+ self,
377
+ fn: AnyFunction,
378
+ uri: str,
379
+ name: str | None = None,
380
+ description: str | None = None,
381
+ mime_type: str | None = None,
382
+ tags: set[str] | None = None,
383
+ ) -> None:
384
+ """Add a resource or template to the server from a function.
385
+
386
+ If the URI contains parameters (e.g. "resource://{param}") or the function
387
+ has parameters, it will be registered as a template resource.
388
+
389
+ Args:
390
+ fn: The function to register as a resource
391
+ uri: The URI for the resource
392
+ name: Optional name for the resource
393
+ description: Optional description of the resource
394
+ mime_type: Optional MIME type for the resource
395
+ tags: Optional set of tags for categorizing the resource
396
+ """
397
+ self._resource_manager.add_resource_or_template_from_fn(
398
+ fn=fn,
399
+ uri=uri,
400
+ name=name,
401
+ description=description,
402
+ mime_type=mime_type,
403
+ tags=tags,
404
+ )
405
+
293
406
  def resource(
294
407
  self,
295
408
  uri: str,
@@ -297,6 +410,7 @@ class FastMCP(Generic[LifespanResultT]):
297
410
  name: str | None = None,
298
411
  description: str | None = None,
299
412
  mime_type: str | None = None,
413
+ tags: set[str] | None = None,
300
414
  ) -> Callable[[AnyFunction], AnyFunction]:
301
415
  """Decorator to register a function as a resource.
302
416
 
@@ -314,6 +428,7 @@ class FastMCP(Generic[LifespanResultT]):
314
428
  name: Optional name for the resource
315
429
  description: Optional description of the resource
316
430
  mime_type: Optional MIME type for the resource
431
+ tags: Optional set of tags for categorizing the resource
317
432
 
318
433
  Example:
319
434
  @server.resource("resource://my-resource")
@@ -342,59 +457,49 @@ class FastMCP(Generic[LifespanResultT]):
342
457
  )
343
458
 
344
459
  def decorator(fn: AnyFunction) -> AnyFunction:
345
- # Check if this should be a template
346
- has_uri_params = "{" in uri and "}" in uri
347
- has_func_params = bool(inspect.signature(fn).parameters)
348
-
349
- if has_uri_params or has_func_params:
350
- # Validate that URI params match function params
351
- uri_params = set(re.findall(r"{(\w+)}", uri))
352
- func_params = set(inspect.signature(fn).parameters.keys())
353
-
354
- if uri_params != func_params:
355
- raise ValueError(
356
- f"Mismatch between URI parameters {uri_params} "
357
- f"and function parameters {func_params}"
358
- )
359
-
360
- # Register as template
361
- self._resource_manager.add_template(
362
- fn=fn,
363
- uri_template=uri,
364
- name=name,
365
- description=description,
366
- mime_type=mime_type or "text/plain",
367
- )
368
- else:
369
- # Register as regular resource
370
- resource = FunctionResource(
371
- uri=AnyUrl(uri),
372
- name=name,
373
- description=description,
374
- mime_type=mime_type or "text/plain",
375
- fn=fn,
376
- )
377
- self.add_resource(resource)
378
- return fn
460
+ self._resource_manager.add_resource_or_template_from_fn(
461
+ fn=fn,
462
+ uri=uri,
463
+ name=name,
464
+ description=description,
465
+ mime_type=mime_type,
466
+ tags=tags,
467
+ )
468
+ return DecoratedFunction(fn)
379
469
 
380
470
  return decorator
381
471
 
382
- def add_prompt(self, prompt: Prompt) -> None:
472
+ def add_prompt(
473
+ self,
474
+ fn: Callable[..., PromptResult | Awaitable[PromptResult]],
475
+ name: str | None = None,
476
+ description: str | None = None,
477
+ tags: set[str] | None = None,
478
+ ) -> None:
383
479
  """Add a prompt to the server.
384
480
 
385
481
  Args:
386
482
  prompt: A Prompt instance to add
387
483
  """
388
- self._prompt_manager.add_prompt(prompt)
484
+ self._prompt_manager.add_prompt_from_fn(
485
+ fn=fn,
486
+ name=name,
487
+ description=description,
488
+ tags=tags,
489
+ )
389
490
 
390
491
  def prompt(
391
- self, name: str | None = None, description: str | None = None
492
+ self,
493
+ name: str | None = None,
494
+ description: str | None = None,
495
+ tags: set[str] | None = None,
392
496
  ) -> Callable[[AnyFunction], AnyFunction]:
393
497
  """Decorator to register a prompt.
394
498
 
395
499
  Args:
396
500
  name: Optional name for the prompt (defaults to function name)
397
501
  description: Optional description of what the prompt does
502
+ tags: Optional set of tags for categorizing the prompt
398
503
 
399
504
  Example:
400
505
  @server.prompt()
@@ -431,9 +536,8 @@ class FastMCP(Generic[LifespanResultT]):
431
536
  )
432
537
 
433
538
  def decorator(func: AnyFunction) -> AnyFunction:
434
- prompt = Prompt.from_function(func, name=name, description=description)
435
- self.add_prompt(prompt)
436
- return func
539
+ self.add_prompt(func, name=name, description=description, tags=tags)
540
+ return DecoratedFunction(func)
437
541
 
438
542
  return decorator
439
543
 
@@ -446,15 +550,20 @@ class FastMCP(Generic[LifespanResultT]):
446
550
  self._mcp_server.create_initialization_options(),
447
551
  )
448
552
 
449
- async def run_sse_async(self) -> None:
553
+ async def run_sse_async(
554
+ self,
555
+ host: str | None = None,
556
+ port: int | None = None,
557
+ log_level: str | None = None,
558
+ ) -> None:
450
559
  """Run the server using SSE transport."""
451
560
  starlette_app = self.sse_app()
452
561
 
453
562
  config = uvicorn.Config(
454
563
  starlette_app,
455
- host=self.settings.host,
456
- port=self.settings.port,
457
- log_level=self.settings.log_level.lower(),
564
+ host=host or self.settings.host,
565
+ port=port or self.settings.port,
566
+ log_level=log_level or self.settings.log_level.lower(),
458
567
  )
459
568
  server = uvicorn.Server(config)
460
569
  await server.serve()
@@ -483,9 +592,20 @@ class FastMCP(Generic[LifespanResultT]):
483
592
  ],
484
593
  )
485
594
 
486
- async def list_prompts(self) -> list[MCPPrompt]:
487
- """List all available prompts."""
488
- prompts = self._prompt_manager.list_prompts()
595
+ def list_prompts(self) -> list[Prompt]:
596
+ """
597
+ List all available prompts.
598
+ """
599
+ return self._prompt_manager.list_prompts()
600
+
601
+ async def _mcp_list_prompts(self) -> list[MCPPrompt]:
602
+ """
603
+ List all available prompts, in the format expected by the low-level MCP
604
+ server.
605
+
606
+ See `list_prompts` for a more ergonomic way to list prompts.
607
+ """
608
+ prompts = self.list_prompts()
489
609
  return [
490
610
  MCPPrompt(
491
611
  name=prompt.name,
@@ -504,47 +624,77 @@ class FastMCP(Generic[LifespanResultT]):
504
624
 
505
625
  async def get_prompt(
506
626
  self, name: str, arguments: dict[str, Any] | None = None
507
- ) -> GetPromptResult:
627
+ ) -> list[Message]:
508
628
  """Get a prompt by name with arguments."""
629
+ return await self._prompt_manager.render_prompt(name, arguments)
630
+
631
+ async def _mcp_get_prompt(
632
+ self, name: str, arguments: dict[str, Any] | None = None
633
+ ) -> GetPromptResult:
634
+ """
635
+ Get a prompt by name with arguments, in the format expected by the low-level
636
+ MCP server.
637
+
638
+ See `get_prompt` for a more ergonomic way to get prompts.
639
+ """
509
640
  try:
510
- messages = await self._prompt_manager.render_prompt(name, arguments)
641
+ messages = await self.get_prompt(name, arguments)
511
642
 
512
643
  return GetPromptResult(messages=pydantic_core.to_jsonable_python(messages))
513
644
  except Exception as e:
514
645
  logger.error(f"Error getting prompt {name}: {e}")
515
646
  raise ValueError(str(e))
516
647
 
517
- def mount(self, prefix: str, app: "FastMCP") -> None:
648
+ def mount(
649
+ self,
650
+ prefix: str,
651
+ app: "FastMCP",
652
+ tool_separator: str | None = None,
653
+ resource_separator: str | None = None,
654
+ prompt_separator: str | None = None,
655
+ ) -> None:
518
656
  """Mount another FastMCP application with a given prefix.
519
657
 
520
658
  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
659
+ - The tools are imported with prefixed names using the tool_separator
660
+ Example: If app has a tool named "get_weather", it will be available as "weatherget_weather"
661
+ - The resources are imported with prefixed URIs using the resource_separator
524
662
  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
663
+ - The templates are imported with prefixed URI templates using the resource_separator
526
664
  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"
665
+ - The prompts are imported with prefixed names using the prompt_separator
666
+ Example: If app has a prompt named "weather_prompt", it will be available as "weather_weather_prompt"
667
+ - The mounted app's lifespan will be executed when the parent app's lifespan runs,
668
+ ensuring that any setup needed by the mounted app is performed
529
669
 
530
670
  Args:
531
671
  prefix: The prefix to use for the mounted application
532
672
  app: The FastMCP application to mount
673
+ tool_separator: Separator for tool names (defaults to "_")
674
+ resource_separator: Separator for resource URIs (defaults to "+")
675
+ prompt_separator: Separator for prompt names (defaults to "_")
533
676
  """
677
+ if tool_separator is None:
678
+ tool_separator = "_"
679
+ if resource_separator is None:
680
+ resource_separator = "+"
681
+ if prompt_separator is None:
682
+ prompt_separator = "_"
683
+
534
684
  # Mount the app in the list of mounted apps
535
685
  self._mounted_apps[prefix] = app
536
686
 
537
- # Import tools from the mounted app with / delimiter
538
- tool_prefix = f"{prefix}/"
687
+ # Import tools from the mounted app
688
+ tool_prefix = f"{prefix}{tool_separator}"
539
689
  self._tool_manager.import_tools(app._tool_manager, tool_prefix)
540
690
 
541
- # Import resources and templates from the mounted app with + delimiter
542
- resource_prefix = f"{prefix}+"
691
+ # Import resources and templates from the mounted app
692
+ resource_prefix = f"{prefix}{resource_separator}"
543
693
  self._resource_manager.import_resources(app._resource_manager, resource_prefix)
544
694
  self._resource_manager.import_templates(app._resource_manager, resource_prefix)
545
695
 
546
- # Import prompts with / delimiter
547
- prompt_prefix = f"{prefix}/"
696
+ # Import prompts from the mounted app
697
+ prompt_prefix = f"{prefix}{prompt_separator}"
548
698
  self._prompt_manager.import_prompts(app._prompt_manager, prompt_prefix)
549
699
 
550
700
  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"]