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