fastmcp 2.1.0__py3-none-any.whl → 2.1.2__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,9 +1,7 @@
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
4
+ from collections.abc import AsyncIterator, Awaitable, Callable
7
5
  from contextlib import (
8
6
  AbstractAsyncContextManager,
9
7
  AsyncExitStack,
@@ -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
 
@@ -166,22 +168,32 @@ class FastMCP(Generic[LifespanResultT]):
166
168
  Args:
167
169
  transport: Transport protocol to use ("stdio" or "sse")
168
170
  """
171
+ logger.info(f'Starting server "{self.name}"...')
169
172
  anyio.run(self.run_async, transport)
170
173
 
171
174
  def _setup_handlers(self) -> None:
172
175
  """Set up core MCP protocol handlers."""
173
- self._mcp_server.list_tools()(self.list_tools)
176
+ self._mcp_server.list_tools()(self._mcp_list_tools)
174
177
  self._mcp_server.call_tool()(self.call_tool)
175
- self._mcp_server.list_resources()(self.list_resources)
176
- self._mcp_server.read_resource()(self.read_resource)
177
- self._mcp_server.list_prompts()(self.list_prompts)
178
- self._mcp_server.get_prompt()(self.get_prompt)
179
- self._mcp_server.list_resource_templates()(self.list_resource_templates)
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)
180
183
 
181
- async def list_tools(self) -> list[MCPTool]:
182
- """List all available tools."""
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()
183
196
 
184
- tools = self._tool_manager.list_tools()
185
197
  return [
186
198
  MCPTool(
187
199
  name=info.name,
@@ -214,10 +226,18 @@ class FastMCP(Generic[LifespanResultT]):
214
226
  converted_result = _convert_to_content(result)
215
227
  return converted_result
216
228
 
217
- async def list_resources(self) -> list[MCPResource]:
218
- """List all available resources."""
229
+ def list_resources(self) -> list[Resource]:
230
+ return self._resource_manager.list_resources()
231
+
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
+ """
219
239
 
220
- resources = self._resource_manager.list_resources()
240
+ resources = self.list_resources()
221
241
  return [
222
242
  MCPResource(
223
243
  uri=resource.uri,
@@ -228,8 +248,18 @@ class FastMCP(Generic[LifespanResultT]):
228
248
  for resource in resources
229
249
  ]
230
250
 
231
- async def list_resource_templates(self) -> list[MCPResourceTemplate]:
232
- 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()
233
263
  return [
234
264
  MCPResourceTemplate(
235
265
  uriTemplate=template.uri_template,
@@ -239,15 +269,27 @@ class FastMCP(Generic[LifespanResultT]):
239
269
  for template in templates
240
270
  ]
241
271
 
242
- async def read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
272
+ async def read_resource(self, uri: AnyUrl | str) -> str | bytes:
243
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
+ """
244
286
 
245
287
  resource = await self._resource_manager.get_resource(uri)
246
288
  if not resource:
247
289
  raise ResourceError(f"Unknown resource: {uri}")
248
290
 
249
291
  try:
250
- content = await resource.read()
292
+ content = await self.read_resource(uri)
251
293
  return [ReadResourceContents(content=content, mime_type=resource.mime_type)]
252
294
  except Exception as e:
253
295
  logger.error(f"Error reading resource {uri}: {e}")
@@ -307,6 +349,7 @@ class FastMCP(Generic[LifespanResultT]):
307
349
  await context.report_progress(50, 100)
308
350
  return str(x)
309
351
  """
352
+
310
353
  # Check if user passed function directly instead of calling decorator
311
354
  if callable(name):
312
355
  raise TypeError(
@@ -316,7 +359,7 @@ class FastMCP(Generic[LifespanResultT]):
316
359
 
317
360
  def decorator(fn: AnyFunction) -> AnyFunction:
318
361
  self.add_tool(fn, name=name, description=description, tags=tags)
319
- return fn
362
+ return DecoratedFunction(fn)
320
363
 
321
364
  return decorator
322
365
 
@@ -326,8 +369,40 @@ class FastMCP(Generic[LifespanResultT]):
326
369
  Args:
327
370
  resource: A Resource instance to add
328
371
  """
372
+
329
373
  self._resource_manager.add_resource(resource)
330
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
+
331
406
  def resource(
332
407
  self,
333
408
  uri: str,
@@ -382,52 +457,36 @@ class FastMCP(Generic[LifespanResultT]):
382
457
  )
383
458
 
384
459
  def decorator(fn: AnyFunction) -> AnyFunction:
385
- # Check if this should be a template
386
- has_uri_params = "{" in uri and "}" in uri
387
- has_func_params = bool(inspect.signature(fn).parameters)
388
-
389
- if has_uri_params or has_func_params:
390
- # Validate that URI params match function params
391
- uri_params = set(re.findall(r"{(\w+)}", uri))
392
- func_params = set(inspect.signature(fn).parameters.keys())
393
-
394
- if uri_params != func_params:
395
- raise ValueError(
396
- f"Mismatch between URI parameters {uri_params} "
397
- f"and function parameters {func_params}"
398
- )
399
-
400
- # Register as template
401
- self._resource_manager.add_template_from_fn(
402
- fn=fn,
403
- uri_template=uri,
404
- name=name,
405
- description=description,
406
- mime_type=mime_type or "text/plain",
407
- tags=tags,
408
- )
409
- else:
410
- # Register as regular resource
411
- resource = FunctionResource(
412
- uri=AnyUrl(uri),
413
- name=name,
414
- description=description,
415
- mime_type=mime_type or "text/plain",
416
- fn=fn,
417
- tags=tags or set(), # Default to empty set if None
418
- )
419
- self.add_resource(resource)
420
- 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)
421
469
 
422
470
  return decorator
423
471
 
424
- 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:
425
479
  """Add a prompt to the server.
426
480
 
427
481
  Args:
428
482
  prompt: A Prompt instance to add
429
483
  """
430
- 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
+ )
431
490
 
432
491
  def prompt(
433
492
  self,
@@ -477,11 +536,8 @@ class FastMCP(Generic[LifespanResultT]):
477
536
  )
478
537
 
479
538
  def decorator(func: AnyFunction) -> AnyFunction:
480
- prompt = Prompt.from_function(
481
- func, name=name, description=description, tags=tags
482
- )
483
- self.add_prompt(prompt)
484
- return func
539
+ self.add_prompt(func, name=name, description=description, tags=tags)
540
+ return DecoratedFunction(func)
485
541
 
486
542
  return decorator
487
543
 
@@ -494,15 +550,20 @@ class FastMCP(Generic[LifespanResultT]):
494
550
  self._mcp_server.create_initialization_options(),
495
551
  )
496
552
 
497
- 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:
498
559
  """Run the server using SSE transport."""
499
560
  starlette_app = self.sse_app()
500
561
 
501
562
  config = uvicorn.Config(
502
563
  starlette_app,
503
- host=self.settings.host,
504
- port=self.settings.port,
505
- 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(),
506
567
  )
507
568
  server = uvicorn.Server(config)
508
569
  await server.serve()
@@ -531,9 +592,20 @@ class FastMCP(Generic[LifespanResultT]):
531
592
  ],
532
593
  )
533
594
 
534
- async def list_prompts(self) -> list[MCPPrompt]:
535
- """List all available prompts."""
536
- 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()
537
609
  return [
538
610
  MCPPrompt(
539
611
  name=prompt.name,
@@ -552,10 +624,21 @@ class FastMCP(Generic[LifespanResultT]):
552
624
 
553
625
  async def get_prompt(
554
626
  self, name: str, arguments: dict[str, Any] | None = None
555
- ) -> GetPromptResult:
627
+ ) -> list[Message]:
556
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
+ """
557
640
  try:
558
- messages = await self._prompt_manager.render_prompt(name, arguments)
641
+ messages = await self.get_prompt(name, arguments)
559
642
 
560
643
  return GetPromptResult(messages=pydantic_core.to_jsonable_python(messages))
561
644
  except Exception as e:
fastmcp/tools/tool.py CHANGED
@@ -5,7 +5,6 @@ from collections.abc import Callable
5
5
  from typing import TYPE_CHECKING, Annotated, Any
6
6
 
7
7
  from pydantic import BaseModel, BeforeValidator, Field
8
- from typing_extensions import Self
9
8
 
10
9
  from fastmcp.exceptions import ToolError
11
10
  from fastmcp.utilities.func_metadata import FuncMetadata, func_metadata
@@ -58,7 +57,10 @@ class Tool(BaseModel):
58
57
  is_async = inspect.iscoroutinefunction(fn)
59
58
 
60
59
  if context_kwarg is None:
61
- sig = inspect.signature(fn)
60
+ if isinstance(fn, classmethod):
61
+ sig = inspect.signature(fn.__func__)
62
+ else:
63
+ sig = inspect.signature(fn)
62
64
  for param_name, param in sig.parameters.items():
63
65
  if param.annotation is Context:
64
66
  context_kwarg = param_name
@@ -99,13 +101,6 @@ class Tool(BaseModel):
99
101
  except Exception as e:
100
102
  raise ToolError(f"Error executing tool {self.name}: {e}") from e
101
103
 
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
104
  def __eq__(self, other: object) -> bool:
110
105
  if not isinstance(other, Tool):
111
106
  return False
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations as _annotations
2
2
 
3
+ import copy
3
4
  from collections.abc import Callable
4
5
  from typing import TYPE_CHECKING, Any
5
6
 
@@ -90,7 +91,9 @@ class ToolManager:
90
91
  for name, tool in tool_manager._tools.items():
91
92
  prefixed_name = f"{prefix}{name}" if prefix else name
92
93
 
93
- new_tool = tool.copy(updates=dict(name=prefixed_name))
94
+ new_tool = copy.copy(tool)
95
+ new_tool.name = prefixed_name
96
+
94
97
  # Store the copied tool
95
98
  self.add_tool(new_tool)
96
99
  logger.debug(f'Imported tool "{name}" as "{prefixed_name}"')
@@ -0,0 +1,101 @@
1
+ import inspect
2
+ from collections.abc import Callable
3
+ from typing import Generic, ParamSpec, TypeVar, cast, overload
4
+
5
+ from typing_extensions import Self
6
+
7
+ R = TypeVar("R")
8
+ P = ParamSpec("P")
9
+
10
+
11
+ class DecoratedFunction(Generic[P, R]):
12
+ """Descriptor for decorated functions.
13
+
14
+ You can return this object from a decorator to ensure that it works across
15
+ all types of functions: vanilla, instance methods, class methods, and static
16
+ methods; both synchronous and asynchronous.
17
+
18
+ This class is used to store the original function and metadata about how to
19
+ register it as a tool.
20
+
21
+ Example usage:
22
+
23
+ ```python
24
+ def my_decorator(fn: Callable[P, R]) -> DecoratedFunction[P, R]:
25
+ return DecoratedFunction(fn)
26
+ ```
27
+
28
+ On a function:
29
+ ```python
30
+ @my_decorator
31
+ def my_function(a: int, b: int) -> int:
32
+ return a + b
33
+ ```
34
+
35
+ On an instance method:
36
+ ```python
37
+ class Test:
38
+ @my_decorator
39
+ def my_function(self, a: int, b: int) -> int:
40
+ return a + b
41
+ ```
42
+
43
+ On a class method:
44
+ ```python
45
+ class Test:
46
+ @classmethod
47
+ @my_decorator
48
+ def my_function(cls, a: int, b: int) -> int:
49
+ return a + b
50
+ ```
51
+
52
+ Note that for classmethods, the decorator must be applied first, then
53
+ `@classmethod` on top.
54
+
55
+ On a static method:
56
+ ```python
57
+ class Test:
58
+ @staticmethod
59
+ @my_decorator
60
+ def my_function(a: int, b: int) -> int:
61
+ return a + b
62
+ ```
63
+ """
64
+
65
+ def __init__(self, fn: Callable[P, R]):
66
+ self.fn = fn
67
+
68
+ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
69
+ """Call the original function."""
70
+ try:
71
+ return self.fn(*args, **kwargs)
72
+ except TypeError as e:
73
+ if "'classmethod' object is not callable" in str(e):
74
+ raise TypeError(
75
+ "To apply this decorator to a classmethod, apply the decorator first, then @classmethod on top."
76
+ )
77
+ raise
78
+
79
+ @overload
80
+ def __get__(self, instance: None, owner: type | None = None) -> Self: ...
81
+
82
+ @overload
83
+ def __get__(
84
+ self, instance: object, owner: type | None = None
85
+ ) -> Callable[P, R]: ...
86
+
87
+ def __get__(
88
+ self, instance: object | None, owner: type | None = None
89
+ ) -> Self | Callable[P, R]:
90
+ """Return the original function when accessed from an instance, or self when accessed from the class."""
91
+ if instance is None:
92
+ return self
93
+ # Return the original function bound to the instance
94
+ return cast(Callable[P, R], self.fn.__get__(instance, owner))
95
+
96
+ def __repr__(self) -> str:
97
+ """Return a representation that matches Python's function representation."""
98
+ module = getattr(self.fn, "__module__", "unknown")
99
+ qualname = getattr(self.fn, "__qualname__", str(self.fn))
100
+ sig_str = str(inspect.signature(self.fn))
101
+ return f"<function {module}.{qualname}{sig_str}>"
@@ -125,7 +125,10 @@ def func_metadata(
125
125
  Returns:
126
126
  A pydantic model representing the function's signature.
127
127
  """
128
- sig = _get_typed_signature(func)
128
+ if isinstance(func, classmethod):
129
+ sig = _get_typed_signature(func.__func__)
130
+ else:
131
+ sig = _get_typed_signature(func)
129
132
  params = sig.parameters
130
133
  dynamic_pydantic_model_params: dict[str, Any] = {}
131
134
  globalns = getattr(func, "__globals__", {})
@@ -20,15 +20,23 @@ def get_logger(name: str) -> logging.Logger:
20
20
 
21
21
 
22
22
  def configure_logging(
23
- level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO",
23
+ level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | int = "INFO",
24
24
  ) -> None:
25
25
  """Configure logging for FastMCP.
26
26
 
27
27
  Args:
28
28
  level: the log level to use
29
29
  """
30
- logging.basicConfig(
31
- level=level,
32
- format="%(message)s",
33
- handlers=[RichHandler(console=Console(stderr=True), rich_tracebacks=True)],
34
- )
30
+ # Only configure the FastMCP logger namespace
31
+ handler = RichHandler(console=Console(stderr=True), rich_tracebacks=True)
32
+ formatter = logging.Formatter("%(message)s")
33
+ handler.setFormatter(formatter)
34
+
35
+ fastmcp_logger = logging.getLogger("FastMCP")
36
+ fastmcp_logger.setLevel(level)
37
+
38
+ # Remove any existing handlers to avoid duplicates on reconfiguration
39
+ for hdlr in fastmcp_logger.handlers[:]:
40
+ fastmcp_logger.removeHandler(hdlr)
41
+
42
+ fastmcp_logger.addHandler(handler)