fastmcp 2.6.1__py3-none-any.whl → 2.7.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
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import datetime
6
+ import inspect
6
7
  import re
7
8
  import warnings
8
9
  from collections.abc import AsyncIterator, Awaitable, Callable
@@ -13,7 +14,7 @@ from contextlib import (
13
14
  )
14
15
  from functools import partial
15
16
  from pathlib import Path
16
- from typing import TYPE_CHECKING, Any, Generic, Literal
17
+ from typing import TYPE_CHECKING, Any, Generic, Literal, overload
17
18
 
18
19
  import anyio
19
20
  import httpx
@@ -40,11 +41,12 @@ from starlette.requests import Request
40
41
  from starlette.responses import Response
41
42
  from starlette.routing import BaseRoute, Route
42
43
 
44
+ import fastmcp
43
45
  import fastmcp.server
44
46
  import fastmcp.settings
45
47
  from fastmcp.exceptions import NotFoundError
46
48
  from fastmcp.prompts import Prompt, PromptManager
47
- from fastmcp.prompts.prompt import PromptResult
49
+ from fastmcp.prompts.prompt import FunctionPrompt
48
50
  from fastmcp.resources import Resource, ResourceManager
49
51
  from fastmcp.resources.template import ResourceTemplate
50
52
  from fastmcp.server.auth.auth import OAuthProvider
@@ -55,9 +57,8 @@ from fastmcp.server.http import (
55
57
  create_streamable_http_app,
56
58
  )
57
59
  from fastmcp.tools import ToolManager
58
- from fastmcp.tools.tool import Tool
60
+ from fastmcp.tools.tool import FunctionTool, Tool
59
61
  from fastmcp.utilities.cache import TimedCache
60
- from fastmcp.utilities.decorators import DecoratedFunction
61
62
  from fastmcp.utilities.logging import get_logger
62
63
  from fastmcp.utilities.mcp_config import MCPConfig
63
64
 
@@ -131,6 +132,8 @@ class FastMCP(Generic[LifespanResultT]):
131
132
  tools: list[Tool | Callable[..., Any]] | None = None,
132
133
  **settings: Any,
133
134
  ):
135
+ if cache_expiration_seconds is not None:
136
+ settings["cache_expiration_seconds"] = cache_expiration_seconds
134
137
  self.settings = fastmcp.settings.ServerSettings(**settings)
135
138
 
136
139
  # If mask_error_details is provided, override the settings value
@@ -148,13 +151,14 @@ class FastMCP(Generic[LifespanResultT]):
148
151
  self.tags: set[str] = tags or set()
149
152
  self.dependencies = dependencies
150
153
  self._cache = TimedCache(
151
- expiration=datetime.timedelta(seconds=cache_expiration_seconds or 0)
154
+ expiration=datetime.timedelta(
155
+ seconds=self.settings.cache_expiration_seconds
156
+ )
152
157
  )
153
158
  self._mounted_servers: dict[str, MountedServer] = {}
154
159
  self._additional_http_routes: list[BaseRoute] = []
155
160
  self._tool_manager = ToolManager(
156
161
  duplicate_behavior=on_duplicate_tools,
157
- serializer=tool_serializer,
158
162
  mask_error_details=self.settings.mask_error_details,
159
163
  )
160
164
  self._resource_manager = ResourceManager(
@@ -165,6 +169,7 @@ class FastMCP(Generic[LifespanResultT]):
165
169
  duplicate_behavior=on_duplicate_prompts,
166
170
  mask_error_details=self.settings.mask_error_details,
167
171
  )
172
+ self._tool_serializer = tool_serializer
168
173
 
169
174
  if lifespan is None:
170
175
  self._has_lifespan = False
@@ -183,10 +188,9 @@ class FastMCP(Generic[LifespanResultT]):
183
188
 
184
189
  if tools:
185
190
  for tool in tools:
186
- if isinstance(tool, Tool):
187
- self._tool_manager.add_tool(tool)
188
- else:
189
- self.add_tool(tool)
191
+ if not isinstance(tool, Tool):
192
+ tool = Tool.from_function(tool, serializer=self._tool_serializer)
193
+ self.add_tool(tool)
190
194
 
191
195
  # Set up MCP protocol handlers
192
196
  self._setup_handlers()
@@ -349,18 +353,18 @@ class FastMCP(Generic[LifespanResultT]):
349
353
  """
350
354
 
351
355
  def decorator(
352
- func: Callable[[Request], Awaitable[Response]],
356
+ fn: Callable[[Request], Awaitable[Response]],
353
357
  ) -> Callable[[Request], Awaitable[Response]]:
354
358
  self._additional_http_routes.append(
355
359
  Route(
356
360
  path,
357
- endpoint=func,
361
+ endpoint=fn,
358
362
  methods=methods,
359
363
  name=name,
360
364
  include_in_schema=include_in_schema,
361
365
  )
362
366
  )
363
- return func
367
+ return fn
364
368
 
365
369
  return decorator
366
370
 
@@ -484,38 +488,16 @@ class FastMCP(Generic[LifespanResultT]):
484
488
 
485
489
  raise NotFoundError(f"Unknown prompt: {name}")
486
490
 
487
- def add_tool(
488
- self,
489
- fn: AnyFunction,
490
- name: str | None = None,
491
- description: str | None = None,
492
- tags: set[str] | None = None,
493
- annotations: ToolAnnotations | dict[str, Any] | None = None,
494
- exclude_args: list[str] | None = None,
495
- ) -> None:
491
+ def add_tool(self, tool: Tool) -> None:
496
492
  """Add a tool to the server.
497
493
 
498
494
  The tool function can optionally request a Context object by adding a parameter
499
495
  with the Context type annotation. See the @tool decorator for examples.
500
496
 
501
497
  Args:
502
- fn: The function to register as a tool
503
- name: Optional name for the tool (defaults to function name)
504
- description: Optional description of what the tool does
505
- tags: Optional set of tags for categorizing the tool
506
- annotations: Optional annotations about the tool's behavior
498
+ tool: The Tool instance to register
507
499
  """
508
- if isinstance(annotations, dict):
509
- annotations = ToolAnnotations(**annotations)
510
-
511
- self._tool_manager.add_tool_from_fn(
512
- fn,
513
- name=name,
514
- description=description,
515
- tags=tags,
516
- annotations=annotations,
517
- exclude_args=exclude_args,
518
- )
500
+ self._tool_manager.add_tool(tool)
519
501
  self._cache.clear()
520
502
 
521
503
  def remove_tool(self, name: str) -> None:
@@ -530,61 +512,141 @@ class FastMCP(Generic[LifespanResultT]):
530
512
  self._tool_manager.remove_tool(name)
531
513
  self._cache.clear()
532
514
 
515
+ @overload
516
+ def tool(
517
+ self,
518
+ name_or_fn: AnyFunction,
519
+ *,
520
+ name: str | None = None,
521
+ description: str | None = None,
522
+ tags: set[str] | None = None,
523
+ annotations: ToolAnnotations | dict[str, Any] | None = None,
524
+ exclude_args: list[str] | None = None,
525
+ ) -> FunctionTool: ...
526
+
527
+ @overload
528
+ def tool(
529
+ self,
530
+ name_or_fn: str | None = None,
531
+ *,
532
+ name: str | None = None,
533
+ description: str | None = None,
534
+ tags: set[str] | None = None,
535
+ annotations: ToolAnnotations | dict[str, Any] | None = None,
536
+ exclude_args: list[str] | None = None,
537
+ ) -> Callable[[AnyFunction], FunctionTool]: ...
538
+
533
539
  def tool(
534
540
  self,
541
+ name_or_fn: str | AnyFunction | None = None,
542
+ *,
535
543
  name: str | None = None,
536
544
  description: str | None = None,
537
545
  tags: set[str] | None = None,
538
546
  annotations: ToolAnnotations | dict[str, Any] | None = None,
539
547
  exclude_args: list[str] | None = None,
540
- ) -> Callable[[AnyFunction], AnyFunction]:
548
+ ) -> Callable[[AnyFunction], FunctionTool] | FunctionTool:
541
549
  """Decorator to register a tool.
542
550
 
543
551
  Tools can optionally request a Context object by adding a parameter with the
544
552
  Context type annotation. The context provides access to MCP capabilities like
545
553
  logging, progress reporting, and resource access.
546
554
 
555
+ This decorator supports multiple calling patterns:
556
+ - @server.tool (without parentheses)
557
+ - @server.tool (with empty parentheses)
558
+ - @server.tool("custom_name") (with name as first argument)
559
+ - @server.tool(name="custom_name") (with name as keyword argument)
560
+ - server.tool(function, name="custom_name") (direct function call)
561
+
547
562
  Args:
548
- name: Optional name for the tool (defaults to function name)
563
+ name_or_fn: Either a function (when used as @tool), a string name, or None
549
564
  description: Optional description of what the tool does
550
565
  tags: Optional set of tags for categorizing the tool
551
566
  annotations: Optional annotations about the tool's behavior
567
+ exclude_args: Optional list of argument names to exclude from the tool schema
568
+ name: Optional name for the tool (keyword-only, alternative to name_or_fn)
552
569
 
553
570
  Example:
554
- @server.tool()
571
+ @server.tool
572
+ def my_tool(x: int) -> str:
573
+ return str(x)
574
+
575
+ @server.tool
555
576
  def my_tool(x: int) -> str:
556
577
  return str(x)
557
578
 
558
- @server.tool()
559
- def tool_with_context(x: int, ctx: Context) -> str:
560
- ctx.info(f"Processing {x}")
579
+ @server.tool("custom_name")
580
+ def my_tool(x: int) -> str:
561
581
  return str(x)
562
582
 
563
- @server.tool()
564
- async def async_tool(x: int, context: Context) -> str:
565
- await context.report_progress(50, 100)
583
+ @server.tool(name="custom_name")
584
+ def my_tool(x: int) -> str:
566
585
  return str(x)
586
+
587
+ # Direct function call
588
+ server.tool(my_function, name="custom_name")
567
589
  """
590
+ if isinstance(annotations, dict):
591
+ annotations = ToolAnnotations(**annotations)
568
592
 
569
- # Check if user passed function directly instead of calling decorator
570
- if callable(name):
571
- raise TypeError(
572
- "The @tool decorator was used incorrectly. "
573
- "Did you forget to call it? Use @tool() instead of @tool"
593
+ if isinstance(name_or_fn, classmethod):
594
+ raise ValueError(
595
+ inspect.cleandoc(
596
+ """
597
+ To decorate a classmethod, first define the method and then call
598
+ tool() directly on the method instead of using it as a
599
+ decorator. See https://gofastmcp.com/patterns/decorating-methods
600
+ for examples and more information.
601
+ """
602
+ )
574
603
  )
575
604
 
576
- def decorator(fn: AnyFunction) -> AnyFunction:
577
- self.add_tool(
605
+ # Determine the actual name and function based on the calling pattern
606
+ if inspect.isroutine(name_or_fn):
607
+ # Case 1: @tool (without parens) - function passed directly
608
+ # Case 2: direct call like tool(fn, name="something")
609
+ fn = name_or_fn
610
+ tool_name = name # Use keyword name if provided, otherwise None
611
+
612
+ # Register the tool immediately and return the tool object
613
+ tool = Tool.from_function(
578
614
  fn,
579
- name=name,
615
+ name=tool_name,
580
616
  description=description,
581
617
  tags=tags,
582
618
  annotations=annotations,
583
619
  exclude_args=exclude_args,
620
+ serializer=self._tool_serializer,
621
+ )
622
+ self.add_tool(tool)
623
+ return tool
624
+
625
+ elif isinstance(name_or_fn, str):
626
+ # Case 3: @tool("custom_name") - name passed as first argument
627
+ if name is not None:
628
+ raise TypeError(
629
+ "Cannot specify both a name as first argument and as keyword argument. "
630
+ f"Use either @tool('{name_or_fn}') or @tool(name='{name}'), not both."
631
+ )
632
+ tool_name = name_or_fn
633
+ elif name_or_fn is None:
634
+ # Case 4: @tool or @tool(name="something") - use keyword name
635
+ tool_name = name
636
+ else:
637
+ raise TypeError(
638
+ f"First argument to @tool must be a function, string, or None, got {type(name_or_fn)}"
584
639
  )
585
- return fn
586
640
 
587
- return decorator
641
+ # Return partial for cases where we need to wait for the function
642
+ return partial(
643
+ self.tool,
644
+ name=tool_name,
645
+ description=description,
646
+ tags=tags,
647
+ annotations=annotations,
648
+ exclude_args=exclude_args,
649
+ )
588
650
 
589
651
  def add_resource(self, resource: Resource, key: str | None = None) -> None:
590
652
  """Add a resource to the server.
@@ -596,6 +658,14 @@ class FastMCP(Generic[LifespanResultT]):
596
658
  self._resource_manager.add_resource(resource, key=key)
597
659
  self._cache.clear()
598
660
 
661
+ def add_template(self, template: ResourceTemplate, key: str | None = None) -> None:
662
+ """Add a resource template to the server.
663
+
664
+ Args:
665
+ template: A ResourceTemplate instance to add
666
+ """
667
+ self._resource_manager.add_template(template, key=key)
668
+
599
669
  def add_resource_fn(
600
670
  self,
601
671
  fn: AnyFunction,
@@ -618,6 +688,12 @@ class FastMCP(Generic[LifespanResultT]):
618
688
  mime_type: Optional MIME type for the resource
619
689
  tags: Optional set of tags for categorizing the resource
620
690
  """
691
+ # deprecated since 2.7.0
692
+ warnings.warn(
693
+ "The add_resource_fn method is deprecated. Use the resource decorator instead.",
694
+ DeprecationWarning,
695
+ stacklevel=2,
696
+ )
621
697
  self._resource_manager.add_resource_or_template_from_fn(
622
698
  fn=fn,
623
699
  uri=uri,
@@ -636,7 +712,7 @@ class FastMCP(Generic[LifespanResultT]):
636
712
  description: str | None = None,
637
713
  mime_type: str | None = None,
638
714
  tags: set[str] | None = None,
639
- ) -> Callable[[AnyFunction], AnyFunction]:
715
+ ) -> Callable[[AnyFunction], Resource | ResourceTemplate]:
640
716
  """Decorator to register a function as a resource.
641
717
 
642
718
  The function will be called when the resource is read to generate its content.
@@ -684,64 +760,124 @@ class FastMCP(Generic[LifespanResultT]):
684
760
  return f"Weather for {city}: {data}"
685
761
  """
686
762
  # Check if user passed function directly instead of calling decorator
687
- if callable(uri):
763
+ if inspect.isroutine(uri):
688
764
  raise TypeError(
689
765
  "The @resource decorator was used incorrectly. "
690
766
  "Did you forget to call it? Use @resource('uri') instead of @resource"
691
767
  )
692
768
 
693
- def decorator(fn: AnyFunction) -> AnyFunction:
694
- self.add_resource_fn(
695
- fn=fn,
696
- uri=uri,
697
- name=name,
698
- description=description,
699
- mime_type=mime_type,
700
- tags=tags,
769
+ def decorator(fn: AnyFunction) -> Resource | ResourceTemplate:
770
+ from fastmcp.server.context import Context
771
+
772
+ if isinstance(fn, classmethod): # type: ignore[reportUnnecessaryIsInstance]
773
+ raise ValueError(
774
+ inspect.cleandoc(
775
+ """
776
+ To decorate a classmethod, first define the method and then call
777
+ resource() directly on the method instead of using it as a
778
+ decorator. See https://gofastmcp.com/patterns/decorating-methods
779
+ for examples and more information.
780
+ """
781
+ )
782
+ )
783
+
784
+ # Check if this should be a template
785
+ has_uri_params = "{" in uri and "}" in uri
786
+ # check if the function has any parameters (other than injected context)
787
+ has_func_params = any(
788
+ p
789
+ for p in inspect.signature(fn).parameters.values()
790
+ if p.annotation is not Context
701
791
  )
702
- return fn
792
+
793
+ if has_uri_params or has_func_params:
794
+ template = ResourceTemplate.from_function(
795
+ fn=fn,
796
+ uri_template=uri,
797
+ name=name,
798
+ description=description,
799
+ mime_type=mime_type,
800
+ tags=tags,
801
+ )
802
+ self.add_template(template)
803
+ return template
804
+ elif not has_uri_params and not has_func_params:
805
+ resource = Resource.from_function(
806
+ fn=fn,
807
+ uri=uri,
808
+ name=name,
809
+ description=description,
810
+ mime_type=mime_type,
811
+ tags=tags,
812
+ )
813
+ self.add_resource(resource)
814
+ return resource
815
+ else:
816
+ raise ValueError(
817
+ "Invalid resource or template definition due to a "
818
+ "mismatch between URI parameters and function parameters."
819
+ )
703
820
 
704
821
  return decorator
705
822
 
706
- def add_prompt(
707
- self,
708
- fn: Callable[..., PromptResult | Awaitable[PromptResult]],
709
- name: str | None = None,
710
- description: str | None = None,
711
- tags: set[str] | None = None,
712
- ) -> None:
823
+ def add_prompt(self, prompt: Prompt) -> None:
713
824
  """Add a prompt to the server.
714
825
 
715
826
  Args:
716
827
  prompt: A Prompt instance to add
717
828
  """
718
- self._prompt_manager.add_prompt_from_fn(
719
- fn=fn,
720
- name=name,
721
- description=description,
722
- tags=tags,
723
- )
829
+ self._prompt_manager.add_prompt(prompt)
724
830
  self._cache.clear()
725
831
 
832
+ @overload
726
833
  def prompt(
727
834
  self,
835
+ name_or_fn: AnyFunction,
836
+ *,
728
837
  name: str | None = None,
729
838
  description: str | None = None,
730
839
  tags: set[str] | None = None,
731
- ) -> Callable[[AnyFunction], AnyFunction]:
840
+ ) -> FunctionPrompt: ...
841
+
842
+ @overload
843
+ def prompt(
844
+ self,
845
+ name_or_fn: str | None = None,
846
+ *,
847
+ name: str | None = None,
848
+ description: str | None = None,
849
+ tags: set[str] | None = None,
850
+ ) -> Callable[[AnyFunction], FunctionPrompt]: ...
851
+
852
+ def prompt(
853
+ self,
854
+ name_or_fn: str | AnyFunction | None = None,
855
+ *,
856
+ name: str | None = None,
857
+ description: str | None = None,
858
+ tags: set[str] | None = None,
859
+ ) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt:
732
860
  """Decorator to register a prompt.
733
861
 
734
862
  Prompts can optionally request a Context object by adding a parameter with the
735
863
  Context type annotation. The context provides access to MCP capabilities like
736
864
  logging, progress reporting, and session information.
737
865
 
866
+ This decorator supports multiple calling patterns:
867
+ - @server.prompt (without parentheses)
868
+ - @server.prompt() (with empty parentheses)
869
+ - @server.prompt("custom_name") (with name as first argument)
870
+ - @server.prompt(name="custom_name") (with name as keyword argument)
871
+ - server.prompt(function, name="custom_name") (direct function call)
872
+
738
873
  Args:
739
- name: Optional name for the prompt (defaults to function name)
874
+ name_or_fn: Either a function (when used as @prompt), a string name, or None
740
875
  description: Optional description of what the prompt does
741
876
  tags: Optional set of tags for categorizing the prompt
877
+ name: Optional name for the prompt (keyword-only, alternative to name_or_fn)
742
878
 
743
879
  Example:
744
- @server.prompt()
880
+ @server.prompt
745
881
  def analyze_table(table_name: str) -> list[Message]:
746
882
  schema = read_table_schema(table_name)
747
883
  return [
@@ -762,8 +898,8 @@ class FastMCP(Generic[LifespanResultT]):
762
898
  }
763
899
  ]
764
900
 
765
- @server.prompt()
766
- async def analyze_file(path: str) -> list[Message]:
901
+ @server.prompt("custom_name")
902
+ def analyze_file(path: str) -> list[Message]:
767
903
  content = await read_file(path)
768
904
  return [
769
905
  {
@@ -777,19 +913,68 @@ class FastMCP(Generic[LifespanResultT]):
777
913
  }
778
914
  }
779
915
  ]
916
+
917
+ @server.prompt(name="custom_name")
918
+ def another_prompt(data: str) -> list[Message]:
919
+ return [{"role": "user", "content": data}]
920
+
921
+ # Direct function call
922
+ server.prompt(my_function, name="custom_name")
780
923
  """
781
- # Check if user passed function directly instead of calling decorator
782
- if callable(name):
783
- raise TypeError(
784
- "The @prompt decorator was used incorrectly. "
785
- "Did you forget to call it? Use @prompt() instead of @prompt"
924
+
925
+ if isinstance(name_or_fn, classmethod):
926
+ raise ValueError(
927
+ inspect.cleandoc(
928
+ """
929
+ To decorate a classmethod, first define the method and then call
930
+ prompt() directly on the method instead of using it as a
931
+ decorator. See https://gofastmcp.com/patterns/decorating-methods
932
+ for examples and more information.
933
+ """
934
+ )
786
935
  )
787
936
 
788
- def decorator(func: AnyFunction) -> AnyFunction:
789
- self.add_prompt(func, name=name, description=description, tags=tags)
790
- return DecoratedFunction(func)
937
+ # Determine the actual name and function based on the calling pattern
938
+ if inspect.isroutine(name_or_fn):
939
+ # Case 1: @prompt (without parens) - function passed directly as decorator
940
+ # Case 2: direct call like prompt(fn, name="something")
941
+ fn = name_or_fn
942
+ prompt_name = name # Use keyword name if provided, otherwise None
791
943
 
792
- return decorator
944
+ # Register the prompt immediately
945
+ prompt = Prompt.from_function(
946
+ fn=fn,
947
+ name=prompt_name,
948
+ description=description,
949
+ tags=tags,
950
+ )
951
+ self.add_prompt(prompt)
952
+
953
+ return prompt
954
+
955
+ elif isinstance(name_or_fn, str):
956
+ # Case 3: @prompt("custom_name") - name passed as first argument
957
+ if name is not None:
958
+ raise TypeError(
959
+ "Cannot specify both a name as first argument and as keyword argument. "
960
+ f"Use either @prompt('{name_or_fn}') or @prompt(name='{name}'), not both."
961
+ )
962
+ prompt_name = name_or_fn
963
+ elif name_or_fn is None:
964
+ # Case 4: @prompt() or @prompt(name="something") - use keyword name
965
+ prompt_name = name
966
+ else:
967
+ raise TypeError(
968
+ f"First argument to @prompt must be a function, string, or None, got {type(name_or_fn)}"
969
+ )
970
+
971
+ # Return partial for cases where we need to wait for the function
972
+ return partial(
973
+ self.prompt,
974
+ name=prompt_name,
975
+ description=description,
976
+ tags=tags,
977
+ )
793
978
 
794
979
  async def run_stdio_async(self) -> None:
795
980
  """Run the server using stdio transport."""
fastmcp/settings.py CHANGED
@@ -170,7 +170,7 @@ class ServerSettings(BaseSettings):
170
170
  ),
171
171
  ] = []
172
172
 
173
- # cache settings (for checking mounted servers)
173
+ # cache settings (for getting attributes from servers, used to avoid repeated calls)
174
174
  cache_expiration_seconds: float = 0
175
175
 
176
176
  # StreamableHTTP settings
fastmcp/tools/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- from .tool import Tool
1
+ from .tool import Tool, FunctionTool
2
2
  from .tool_manager import ToolManager
3
3
 
4
- __all__ = ["Tool", "ToolManager"]
4
+ __all__ = ["Tool", "ToolManager", "FunctionTool"]