fastmcp 2.2.1__py3-none-any.whl → 2.2.3__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/cli/cli.py CHANGED
@@ -186,7 +186,7 @@ def version(ctx: Context):
186
186
  "MCP version": importlib.metadata.version("mcp"),
187
187
  "Python version": platform.python_version(),
188
188
  "Platform": platform.platform(),
189
- "FastMCP root path": f"~/{Path(__file__).resolve().parents[3].relative_to(Path.home())}",
189
+ "FastMCP root path": Path(fastmcp.__file__).resolve().parents[1],
190
190
  }
191
191
 
192
192
  g = Table.grid(padding=(0, 1))
fastmcp/client/client.py CHANGED
@@ -45,7 +45,8 @@ class Client:
45
45
  ):
46
46
  self.transport = infer_transport(transport)
47
47
  self._session: ClientSession | None = None
48
- self._session_cms: list[AbstractAsyncContextManager[ClientSession]] = []
48
+ self._session_cm: AbstractAsyncContextManager[ClientSession] | None = None
49
+ self._nesting_counter: int = 0
49
50
 
50
51
  self._session_kwargs: SessionKwargs = {
51
52
  "sampling_callback": None,
@@ -85,29 +86,21 @@ class Client:
85
86
  return self._session is not None
86
87
 
87
88
  async def __aenter__(self):
88
- if self.is_connected():
89
- # We're already connected, no need to add None to the session_cms list
90
- return self
91
-
92
- try:
93
- session_cm = self.transport.connect_session(**self._session_kwargs)
94
- self._session_cms.append(session_cm)
95
- self._session = await self._session_cms[-1].__aenter__()
96
- return self
97
- except Exception as e:
98
- # Ensure cleanup if __aenter__ fails partially
99
- self._session = None
100
- if self._session_cms:
101
- self._session_cms.pop()
102
- raise ConnectionError(
103
- f"Failed to connect using {self.transport}: {e}"
104
- ) from e
89
+ if self._nesting_counter == 0:
90
+ # create new session
91
+ self._session_cm = self.transport.connect_session(**self._session_kwargs)
92
+ self._session = await self._session_cm.__aenter__()
93
+
94
+ self._nesting_counter += 1
95
+ return self
105
96
 
106
97
  async def __aexit__(self, exc_type, exc_val, exc_tb):
107
- if self._session_cms:
108
- await self._session_cms[-1].__aexit__(exc_type, exc_val, exc_tb)
98
+ self._nesting_counter -= 1
99
+
100
+ if self._nesting_counter == 0 and self._session_cm is not None:
101
+ await self._session_cm.__aexit__(exc_type, exc_val, exc_tb)
102
+ self._session_cm = None
109
103
  self._session = None
110
- self._session_cms.pop()
111
104
 
112
105
  # --- MCP Client Methods ---
113
106
  async def ping(self) -> None:
@@ -3,6 +3,7 @@ import contextlib
3
3
  import datetime
4
4
  import os
5
5
  import shutil
6
+ import sys
6
7
  from collections.abc import AsyncIterator
7
8
  from pathlib import Path
8
9
  from typing import (
@@ -185,7 +186,7 @@ class PythonStdioTransport(StdioTransport):
185
186
  args: list[str] | None = None,
186
187
  env: dict[str, str] | None = None,
187
188
  cwd: str | None = None,
188
- python_cmd: str = "python",
189
+ python_cmd: str = sys.executable,
189
190
  ):
190
191
  """
191
192
  Initialize a Python transport.
@@ -0,0 +1,19 @@
1
+ # FastMCP Contrib Modules
2
+
3
+ This directory holds community-contributed modules for FastMCP. These modules extend FastMCP's functionality but are not officially maintained by the core team.
4
+
5
+ **Guarantees:**
6
+ * Modules in `contrib` may have different testing requirements or stability guarantees compared to the core library.
7
+ * Changes to the core FastMCP library might break modules in `contrib` without explicit warnings in the main changelog.
8
+
9
+ Use these modules at your own discretion. Contributions are welcome, but please include tests and documentation.
10
+
11
+ ## Usage
12
+
13
+ To use a contrib module, import it from the `fastmcp.contrib` package.
14
+
15
+ ```python
16
+ from fastmcp.contrib import my_module
17
+ ```
18
+
19
+ Note that the contrib modules may have different dependencies than the core library, which can be noted in their respective README's or even separate requirements / dependency files.
@@ -0,0 +1,35 @@
1
+ # Bulk Tool Caller
2
+
3
+ This module provides the `BulkToolCaller` class, which extends the `MCPMixin` to offer tools for performing multiple tool calls in a single request to a FastMCP server. This can be useful for optimizing interactions with the server by reducing the overhead of individual tool calls.
4
+
5
+ ## Usage
6
+
7
+ To use the `BulkToolCaller`, see the example [example.py](./example.py) file. The `BulkToolCaller` can be instantiated and then registered with a FastMCP server URL. It provides methods to call multiple tools in bulk, either different tools or the same tool with different arguments.
8
+
9
+
10
+ ## Provided Tools
11
+
12
+ The `BulkToolCaller` provides the following tools:
13
+
14
+ ### `call_tools_bulk`
15
+
16
+ Calls multiple different tools registered on the MCP server in a single request.
17
+
18
+ - **Arguments:**
19
+ - `tool_calls` (list of `CallToolRequest`): A list of objects, where each object specifies the `tool` name and `arguments` for an individual tool call.
20
+ - `continue_on_error` (bool, optional): If `True`, continue executing subsequent tool calls even if a previous one resulted in an error. Defaults to `True`.
21
+
22
+ - **Returns:**
23
+ A list of `CallToolRequestResult` objects, each containing the result (`isError`, `content`) and the original `tool` name and `arguments` for each call.
24
+
25
+ ### `call_tool_bulk`
26
+
27
+ Calls a single tool registered on the MCP server multiple times with different arguments in a single request.
28
+
29
+ - **Arguments:**
30
+ - `tool` (str): The name of the tool to call.
31
+ - `tool_arguments` (list of dict): A list of dictionaries, where each dictionary contains the arguments for an individual run of the tool.
32
+ - `continue_on_error` (bool, optional): If `True`, continue executing subsequent tool calls even if a previous one resulted in an error. Defaults to `True`.
33
+
34
+ - **Returns:**
35
+ A list of `CallToolRequestResult` objects, each containing the result (`isError`, `content`) and the original `tool` name and `arguments` for each call.
@@ -0,0 +1,3 @@
1
+ from .bulk_tool_caller import BulkToolCaller
2
+
3
+ __all__ = ["BulkToolCaller"]
@@ -0,0 +1,135 @@
1
+ from typing import Any
2
+
3
+ from mcp.types import CallToolResult
4
+ from pydantic import BaseModel, Field
5
+
6
+ from fastmcp import FastMCP
7
+ from fastmcp.client import Client
8
+ from fastmcp.client.transports import FastMCPTransport
9
+ from fastmcp.contrib.mcp_mixin.mcp_mixin import (
10
+ _DEFAULT_SEPARATOR_TOOL,
11
+ MCPMixin,
12
+ mcp_tool,
13
+ )
14
+
15
+
16
+ class CallToolRequest(BaseModel):
17
+ """A class to represent a request to call a tool with specific arguments."""
18
+
19
+ tool: str = Field(description="The name of the tool to call.")
20
+ arguments: dict[str, Any] = Field(
21
+ description="A dictionary containing the arguments for the tool call."
22
+ )
23
+
24
+
25
+ class CallToolRequestResult(CallToolResult):
26
+ """
27
+ A class to represent the result of a bulk tool call.
28
+ It extends CallToolResult to include information about the requested tool call.
29
+ """
30
+
31
+ tool: str = Field(description="The name of the tool that was called.")
32
+ arguments: dict[str, Any] = Field(
33
+ description="The arguments used for the tool call."
34
+ )
35
+
36
+ @classmethod
37
+ def from_call_tool_result(
38
+ cls, result: CallToolResult, tool: str, arguments: dict[str, Any]
39
+ ) -> "CallToolRequestResult":
40
+ """
41
+ Create a CallToolRequestResult from a CallToolResult.
42
+ """
43
+ return cls(
44
+ tool=tool,
45
+ arguments=arguments,
46
+ isError=result.isError,
47
+ content=result.content,
48
+ )
49
+
50
+
51
+ class BulkToolCaller(MCPMixin):
52
+ """
53
+ A class to provide a "bulk tool call" tool for a FastMCP server
54
+ """
55
+
56
+ def register_tools(
57
+ self,
58
+ mcp_server: "FastMCP",
59
+ prefix: str | None = None,
60
+ separator: str = _DEFAULT_SEPARATOR_TOOL,
61
+ ) -> None:
62
+ """
63
+ Register the tools provided by this class with the given MCP server.
64
+ """
65
+ self.connection = FastMCPTransport(mcp_server)
66
+
67
+ super().register_tools(mcp_server=mcp_server)
68
+
69
+ @mcp_tool()
70
+ async def call_tools_bulk(
71
+ self, tool_calls: list[CallToolRequest], continue_on_error: bool = True
72
+ ) -> list[CallToolRequestResult]:
73
+ """
74
+ Call multiple tools registered on this MCP server in a single request. Each call can
75
+ be for a different tool and can include different arguments. Useful for speeding up
76
+ what would otherwise take several individual tool calls.
77
+ """
78
+ results = []
79
+
80
+ for tool_call in tool_calls:
81
+ result = await self._call_tool(tool_call.tool, tool_call.arguments)
82
+
83
+ results.append(result)
84
+
85
+ if result.isError and not continue_on_error:
86
+ return results
87
+
88
+ return results
89
+
90
+ @mcp_tool()
91
+ async def call_tool_bulk(
92
+ self,
93
+ tool: str,
94
+ tool_arguments: list[dict[str, str | int | float | bool | None]],
95
+ continue_on_error: bool = True,
96
+ ) -> list[CallToolRequestResult]:
97
+ """
98
+ Call a single tool registered on this MCP server multiple times with a single request.
99
+ Each call can include different arguments. Useful for speeding up what would otherwise
100
+ take several individual tool calls.
101
+
102
+ Args:
103
+ tool: The name of the tool to call.
104
+ tool_arguments: A list of dictionaries, where each dictionary contains the arguments for an individual run of the tool.
105
+ """
106
+ results = []
107
+
108
+ for tool_call_arguments in tool_arguments:
109
+ result = await self._call_tool(tool, tool_call_arguments)
110
+
111
+ results.append(result)
112
+
113
+ if result.isError and not continue_on_error:
114
+ return results
115
+
116
+ return results
117
+
118
+ async def _call_tool(
119
+ self, tool: str, arguments: dict[str, Any]
120
+ ) -> CallToolRequestResult:
121
+ """
122
+ Helper method to call a tool with the provided arguments.
123
+ """
124
+
125
+ async with Client(self.connection) as client:
126
+ result = await client.call_tool(
127
+ name=tool, arguments=arguments, _return_raw_result=True
128
+ )
129
+
130
+ return CallToolRequestResult(
131
+ tool=tool,
132
+ arguments=arguments,
133
+ isError=result.isError,
134
+ content=result.content,
135
+ )
@@ -0,0 +1,17 @@
1
+ """Sample code for FastMCP using MCPMixin."""
2
+
3
+ from fastmcp import FastMCP
4
+ from fastmcp.contrib.bulk_tool_caller import BulkToolCaller
5
+
6
+ mcp = FastMCP()
7
+
8
+
9
+ @mcp.tool()
10
+ def echo_tool(text: str) -> str:
11
+ """Echo the input text"""
12
+ return text
13
+
14
+
15
+ bulk_tool_caller = BulkToolCaller()
16
+
17
+ bulk_tool_caller.register_tools(mcp)
@@ -0,0 +1,39 @@
1
+ # MCP Mixin
2
+
3
+ This module provides the `MCPMixin` base class and associated decorators (`@mcp_tool`, `@mcp_resource`, `@mcp_prompt`).
4
+
5
+ It allows developers to easily define classes whose methods can be registered as tools, resources, or prompts with a `FastMCP` server instance using the `register_all()`, `register_tools()`, `register_resources()`, or `register_prompts()` methods provided by the mixin.
6
+
7
+ ## Usage
8
+
9
+ Inherit from `MCPMixin` and use the decorators on the methods you want to register.
10
+
11
+ ```python
12
+ from fastmcp import FastMCP
13
+ from fastmcp.contrib.mcp_mixin import MCPMixin, mcp_tool, mcp_resource
14
+
15
+ class MyComponent(MCPMixin):
16
+ @mcp_tool(name="my_tool", description="Does something cool.")
17
+ def tool_method(self):
18
+ return "Tool executed!"
19
+
20
+ @mcp_resource(uri="component://data")
21
+ def resource_method(self):
22
+ return {"data": "some data"}
23
+
24
+ mcp_server = FastMCP()
25
+ component = MyComponent()
26
+
27
+ # Register all decorated methods with a prefix
28
+ # Useful if you will have multiple instantiated objects of the same class
29
+ # and want to avoid name collisions.
30
+ component.register_all(mcp_server, prefix="my_comp")
31
+
32
+ # Register without a prefix
33
+ # component.register_all(mcp_server)
34
+
35
+ # Now 'my_comp_my_tool' tool and 'my_comp+component://data' resource are registered (if prefix used)
36
+ # Or 'my_tool' and 'component://data' are registered (if no prefix used)
37
+ ```
38
+
39
+ The `prefix` argument in registration methods is optional. If omitted, methods are registered with their original decorated names/URIs. Individual separators (`tools_separator`, `resources_separator`, `prompts_separator`) can also be provided to `register_all` to change the separator for specific types.
@@ -0,0 +1,8 @@
1
+ from .mcp_mixin import MCPMixin, mcp_tool, mcp_resource, mcp_prompt
2
+
3
+ __all__ = [
4
+ "MCPMixin",
5
+ "mcp_tool",
6
+ "mcp_resource",
7
+ "mcp_prompt",
8
+ ]
@@ -0,0 +1,52 @@
1
+ """Sample code for FastMCP using MCPMixin."""
2
+
3
+ import asyncio
4
+
5
+ from fastmcp import FastMCP
6
+ from fastmcp.contrib.mcp_mixin import (
7
+ MCPMixin,
8
+ mcp_prompt,
9
+ mcp_resource,
10
+ mcp_tool,
11
+ )
12
+
13
+ mcp = FastMCP()
14
+
15
+
16
+ class Sample(MCPMixin):
17
+ def __init__(self, name):
18
+ self.name = name
19
+
20
+ @mcp_tool()
21
+ def first_tool(self):
22
+ """First tool description."""
23
+ return f"Executed tool {self.name}."
24
+
25
+ @mcp_resource(uri="test://test")
26
+ def first_resource(self):
27
+ """First resource description."""
28
+ return f"Executed resource {self.name}."
29
+
30
+ @mcp_prompt()
31
+ def first_prompt(self):
32
+ """First prompt description."""
33
+ return f"here's a prompt! {self.name}."
34
+
35
+
36
+ first_sample = Sample("First")
37
+ second_sample = Sample("Second")
38
+
39
+ first_sample.register_all(mcp_server=mcp, prefix="first")
40
+ second_sample.register_all(mcp_server=mcp, prefix="second")
41
+
42
+
43
+ async def list_components():
44
+ print("MCP Server running with registered components...")
45
+ print("Tools:", list(await mcp.get_tools()))
46
+ print("Resources:", list(await mcp.get_resources()))
47
+ print("Prompts:", list(await mcp.get_prompts()))
48
+
49
+
50
+ if __name__ == "__main__":
51
+ asyncio.run(list_components())
52
+ mcp.run()
@@ -0,0 +1,208 @@
1
+ """Provides a base mixin class and decorators for easy registration of class methods with FastMCP."""
2
+
3
+ from collections.abc import Callable
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ if TYPE_CHECKING:
7
+ from fastmcp.server import FastMCP
8
+
9
+ _MCP_REGISTRATION_TOOL_ATTR = "_mcp_tool_registration"
10
+ _MCP_REGISTRATION_RESOURCE_ATTR = "_mcp_resource_registration"
11
+ _MCP_REGISTRATION_PROMPT_ATTR = "_mcp_prompt_registration"
12
+
13
+ _DEFAULT_SEPARATOR_TOOL = "_"
14
+ _DEFAULT_SEPARATOR_RESOURCE = "+"
15
+ _DEFAULT_SEPARATOR_PROMPT = "_"
16
+
17
+
18
+ def mcp_tool(
19
+ name: str | None = None,
20
+ description: str | None = None,
21
+ tags: set[str] | None = None,
22
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
23
+ """Decorator to mark a method as an MCP tool for later registration."""
24
+
25
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
26
+ call_args = {
27
+ "name": name or func.__name__,
28
+ "description": description,
29
+ "tags": tags,
30
+ }
31
+ call_args = {k: v for k, v in call_args.items() if v is not None}
32
+ setattr(func, _MCP_REGISTRATION_TOOL_ATTR, call_args)
33
+ return func
34
+
35
+ return decorator
36
+
37
+
38
+ def mcp_resource(
39
+ uri: str,
40
+ *,
41
+ name: str | None = None,
42
+ description: str | None = None,
43
+ mime_type: str | None = None,
44
+ tags: set[str] | None = None,
45
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
46
+ """Decorator to mark a method as an MCP resource for later registration."""
47
+
48
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
49
+ call_args = {
50
+ "uri": uri,
51
+ "name": name or func.__name__,
52
+ "description": description,
53
+ "mime_type": mime_type,
54
+ "tags": tags,
55
+ }
56
+ call_args = {k: v for k, v in call_args.items() if v is not None}
57
+
58
+ setattr(func, _MCP_REGISTRATION_RESOURCE_ATTR, call_args)
59
+
60
+ return func
61
+
62
+ return decorator
63
+
64
+
65
+ def mcp_prompt(
66
+ name: str | None = None,
67
+ description: str | None = None,
68
+ tags: set[str] | None = None,
69
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
70
+ """Decorator to mark a method as an MCP prompt for later registration."""
71
+
72
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
73
+ call_args = {
74
+ "name": name or func.__name__,
75
+ "description": description,
76
+ "tags": tags,
77
+ }
78
+
79
+ call_args = {k: v for k, v in call_args.items() if v is not None}
80
+
81
+ setattr(func, _MCP_REGISTRATION_PROMPT_ATTR, call_args)
82
+ return func
83
+
84
+ return decorator
85
+
86
+
87
+ class MCPMixin:
88
+ """Base mixin class for objects that can register tools, resources, and prompts
89
+ with a FastMCP server instance using decorators.
90
+
91
+ This mixin provides methods like `register_all`, `register_tools`, etc.,
92
+ which iterate over the methods of the inheriting class, find methods
93
+ decorated with `@mcp_tool`, `@mcp_resource`, or `@mcp_prompt`, and
94
+ register them with the provided FastMCP server instance.
95
+ """
96
+
97
+ def _get_methods_to_register(self, registration_type: str):
98
+ """Retrieves all methods marked for a specific registration type."""
99
+ return [
100
+ (
101
+ getattr(self, method_name),
102
+ getattr(getattr(self, method_name), registration_type).copy(),
103
+ )
104
+ for method_name in dir(self)
105
+ if callable(getattr(self, method_name))
106
+ and hasattr(getattr(self, method_name), registration_type)
107
+ ]
108
+
109
+ def register_tools(
110
+ self,
111
+ mcp_server: "FastMCP",
112
+ prefix: str | None = None,
113
+ separator: str = _DEFAULT_SEPARATOR_TOOL,
114
+ ) -> None:
115
+ """Registers all methods marked with @mcp_tool with the FastMCP server.
116
+
117
+ Args:
118
+ mcp_server: The FastMCP server instance to register tools with.
119
+ prefix: Optional prefix to prepend to tool names. If provided, the
120
+ final name will be f"{prefix}{separator}{original_name}".
121
+ separator: The separator string used between prefix and original name.
122
+ Defaults to '_'.
123
+ """
124
+ for method, registration_info in self._get_methods_to_register(
125
+ _MCP_REGISTRATION_TOOL_ATTR
126
+ ):
127
+ if prefix:
128
+ registration_info["name"] = (
129
+ f"{prefix}{separator}{registration_info['name']}"
130
+ )
131
+ mcp_server.add_tool(fn=method, **registration_info)
132
+
133
+ def register_resources(
134
+ self,
135
+ mcp_server: "FastMCP",
136
+ prefix: str | None = None,
137
+ separator: str = _DEFAULT_SEPARATOR_RESOURCE,
138
+ ) -> None:
139
+ """Registers all methods marked with @mcp_resource with the FastMCP server.
140
+
141
+ Args:
142
+ mcp_server: The FastMCP server instance to register resources with.
143
+ prefix: Optional prefix to prepend to resource names and URIs. If provided,
144
+ the final name will be f"{prefix}{separator}{original_name}" and the
145
+ final URI will be f"{prefix}{separator}{original_uri}".
146
+ separator: The separator string used between prefix and original name/URI.
147
+ Defaults to '+'.
148
+ """
149
+ for method, registration_info in self._get_methods_to_register(
150
+ _MCP_REGISTRATION_RESOURCE_ATTR
151
+ ):
152
+ if prefix:
153
+ registration_info["name"] = (
154
+ f"{prefix}{separator}{registration_info['name']}"
155
+ )
156
+ registration_info["uri"] = (
157
+ f"{prefix}{separator}{registration_info['uri']}"
158
+ )
159
+ mcp_server.add_resource_fn(fn=method, **registration_info)
160
+
161
+ def register_prompts(
162
+ self,
163
+ mcp_server: "FastMCP",
164
+ prefix: str | None = None,
165
+ separator: str = _DEFAULT_SEPARATOR_PROMPT,
166
+ ) -> None:
167
+ """Registers all methods marked with @mcp_prompt with the FastMCP server.
168
+
169
+ Args:
170
+ mcp_server: The FastMCP server instance to register prompts with.
171
+ prefix: Optional prefix to prepend to prompt names. If provided, the
172
+ final name will be f"{prefix}{separator}{original_name}".
173
+ separator: The separator string used between prefix and original name.
174
+ Defaults to '_'.
175
+ """
176
+ for method, registration_info in self._get_methods_to_register(
177
+ _MCP_REGISTRATION_PROMPT_ATTR
178
+ ):
179
+ if prefix:
180
+ registration_info["name"] = (
181
+ f"{prefix}{separator}{registration_info['name']}"
182
+ )
183
+ mcp_server.add_prompt(fn=method, **registration_info)
184
+
185
+ def register_all(
186
+ self,
187
+ mcp_server: "FastMCP",
188
+ prefix: str | None = None,
189
+ tool_separator: str = _DEFAULT_SEPARATOR_TOOL,
190
+ resource_separator: str = _DEFAULT_SEPARATOR_RESOURCE,
191
+ prompt_separator: str = _DEFAULT_SEPARATOR_PROMPT,
192
+ ) -> None:
193
+ """Registers all marked tools, resources, and prompts with the server.
194
+
195
+ This method calls `register_tools`, `register_resources`, and `register_prompts`
196
+ internally, passing the provided prefix and separators.
197
+
198
+ Args:
199
+ mcp_server: The FastMCP server instance to register with.
200
+ prefix: Optional prefix applied to all registered items unless overridden
201
+ by a specific separator argument.
202
+ tool_separator: Separator for tool names (defaults to '_').
203
+ resource_separator: Separator for resource names/URIs (defaults to '+').
204
+ prompt_separator: Separator for prompt names (defaults to '_').
205
+ """
206
+ self.register_tools(mcp_server, prefix=prefix, separator=tool_separator)
207
+ self.register_resources(mcp_server, prefix=prefix, separator=resource_separator)
208
+ self.register_prompts(mcp_server, prefix=prefix, separator=prompt_separator)
@@ -24,13 +24,16 @@ from fastmcp.utilities.types import _convert_set_defaults
24
24
 
25
25
 
26
26
  def build_regex(template: str) -> re.Pattern:
27
- # Escape all non-brace characters, then restore {var} placeholders
28
27
  parts = re.split(r"(\{[^}]+\})", template)
29
28
  pattern = ""
30
29
  for part in parts:
31
30
  if part.startswith("{") and part.endswith("}"):
32
31
  name = part[1:-1]
33
- pattern += f"(?P<{name}>[^/]+)"
32
+ if name.endswith("*"):
33
+ name = name[:-1]
34
+ pattern += f"(?P<{name}>.+)"
35
+ else:
36
+ pattern += f"(?P<{name}>[^/]+)"
34
37
  else:
35
38
  pattern += re.escape(part)
36
39
  return re.compile(f"^{pattern}$")
fastmcp/server/proxy.py CHANGED
@@ -3,7 +3,9 @@ from urllib.parse import quote
3
3
 
4
4
  import mcp.types
5
5
  from mcp.server.lowlevel.helper_types import ReadResourceContents
6
+ from mcp.shared.exceptions import McpError
6
7
  from mcp.types import (
8
+ METHOD_NOT_FOUND,
7
9
  BlobResourceContents,
8
10
  EmbeddedResource,
9
11
  GetPromptResult,
@@ -173,7 +175,14 @@ class FastMCPProxy(FastMCP):
173
175
  tools = await super().get_tools()
174
176
 
175
177
  async with self.client:
176
- for tool in await self.client.list_tools():
178
+ try:
179
+ client_tools = await self.client.list_tools()
180
+ except McpError as e:
181
+ if e.error.code == METHOD_NOT_FOUND:
182
+ client_tools = []
183
+ else:
184
+ raise e
185
+ for tool in client_tools:
177
186
  tool_proxy = await ProxyTool.from_client(self.client, tool)
178
187
  tools[tool_proxy.name] = tool_proxy
179
188
 
@@ -183,7 +192,14 @@ class FastMCPProxy(FastMCP):
183
192
  resources = await super().get_resources()
184
193
 
185
194
  async with self.client:
186
- for resource in await self.client.list_resources():
195
+ try:
196
+ client_resources = await self.client.list_resources()
197
+ except McpError as e:
198
+ if e.error.code == METHOD_NOT_FOUND:
199
+ client_resources = []
200
+ else:
201
+ raise e
202
+ for resource in client_resources:
187
203
  resource_proxy = await ProxyResource.from_client(self.client, resource)
188
204
  resources[str(resource_proxy.uri)] = resource_proxy
189
205
 
@@ -193,7 +209,14 @@ class FastMCPProxy(FastMCP):
193
209
  templates = await super().get_resource_templates()
194
210
 
195
211
  async with self.client:
196
- for template in await self.client.list_resource_templates():
212
+ try:
213
+ client_templates = await self.client.list_resource_templates()
214
+ except McpError as e:
215
+ if e.error.code == METHOD_NOT_FOUND:
216
+ client_templates = []
217
+ else:
218
+ raise e
219
+ for template in client_templates:
197
220
  template_proxy = await ProxyTemplate.from_client(self.client, template)
198
221
  templates[template_proxy.uri_template] = template_proxy
199
222
 
@@ -203,7 +226,14 @@ class FastMCPProxy(FastMCP):
203
226
  prompts = await super().get_prompts()
204
227
 
205
228
  async with self.client:
206
- for prompt in await self.client.list_prompts():
229
+ try:
230
+ client_prompts = await self.client.list_prompts()
231
+ except McpError as e:
232
+ if e.error.code == METHOD_NOT_FOUND:
233
+ client_prompts = []
234
+ else:
235
+ raise e
236
+ for prompt in client_prompts:
207
237
  prompt_proxy = await ProxyPrompt.from_client(self.client, prompt)
208
238
  prompts[prompt_proxy.name] = prompt_proxy
209
239
  return prompts
fastmcp/tools/tool.py CHANGED
@@ -76,7 +76,12 @@ class Tool(BaseModel):
76
76
  fn_callable,
77
77
  skip_names=[context_kwarg] if context_kwarg is not None else [],
78
78
  )
79
- parameters = func_arg_metadata.arg_model.model_json_schema()
79
+ try:
80
+ parameters = func_arg_metadata.arg_model.model_json_schema()
81
+ except Exception as e:
82
+ raise TypeError(
83
+ f'Unable to parse parameters for function "{fn.__name__}": {e}'
84
+ ) from e
80
85
 
81
86
  return cls(
82
87
  fn=fn_callable,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastmcp
3
- Version: 2.2.1
3
+ Version: 2.2.3
4
4
  Summary: The fast, Pythonic way to build MCP servers.
5
5
  Project-URL: Homepage, https://gofastmcp.com
6
6
  Project-URL: Repository, https://github.com/jlowin/fastmcp
@@ -361,32 +361,32 @@ The `Context` object provides:
361
361
 
362
362
  ### Images
363
363
 
364
- Easily handle image input and output using the `fastmcp.Image` helper class.
364
+ Easily handle image outputs using the `fastmcp.Image` helper class.
365
+
366
+ <Tip>
367
+ The below code requires the `pillow` library to be installed.
368
+ </Tip>
365
369
 
366
370
  ```python
367
- from fastmcp import FastMCP, Image
368
- from PIL import Image as PILImage
369
- import io
371
+ from mcp.server.fastmcp import FastMCP, Image
372
+ from io import BytesIO
373
+ try:
374
+ from PIL import Image as PILImage
375
+ except ImportError:
376
+ raise ImportError("Please install the `pillow` library to run this example.")
370
377
 
371
- mcp = FastMCP("Image Demo")
378
+ mcp = FastMCP("My App")
372
379
 
373
380
  @mcp.tool()
374
- def create_thumbnail(image_data: Image) -> Image:
375
- """Creates a 100x100 thumbnail from the provided image."""
376
- img = PILImage.open(io.BytesIO(image_data.data)) # Assumes image_data received as Image with bytes
377
- img.thumbnail((100, 100))
378
- buffer = io.BytesIO()
381
+ def create_thumbnail(image_path: str) -> Image:
382
+ """Create a thumbnail from an image"""
383
+ img = PILImage.open(image_path)
384
+ img.thumbnail((100, 100))
385
+ buffer = BytesIO()
379
386
  img.save(buffer, format="PNG")
380
- # Return a new Image object with the thumbnail data
381
387
  return Image(data=buffer.getvalue(), format="png")
382
-
383
- @mcp.tool()
384
- def load_image_from_disk(path: str) -> Image:
385
- """Loads an image from the specified path."""
386
- # Handles reading file and detecting format based on extension
387
- return Image(path=path)
388
388
  ```
389
- FastMCP handles the conversion to/from the base64-encoded format required by the MCP protocol.
389
+ Return the `Image` helper class from your tool to send an image to the client. The `Image` helper class handles the conversion to/from the base64-encoded format required by the MCP protocol. It works with either a path to an image file, or a bytes object.
390
390
 
391
391
 
392
392
  ### MCP Clients
@@ -4,28 +4,37 @@ fastmcp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  fastmcp/settings.py,sha256=VCjc-3011pKRYjt2h9rZ68XhVEekbpyLyVUREVBTSrg,1955
5
5
  fastmcp/cli/__init__.py,sha256=Ii284TNoG5lxTP40ETMGhHEq3lQZWxu9m9JuU57kUpQ,87
6
6
  fastmcp/cli/claude.py,sha256=IAlcZ4qZKBBj09jZUMEx7EANZE_IR3vcu7zOBJmMOuU,4567
7
- fastmcp/cli/cli.py,sha256=vdnJi_zz4ZYoPxp9xlJbh6RlGogaFY3icaOPzO_xsLE,14874
7
+ fastmcp/cli/cli.py,sha256=ESyqSl7rxQKoJokkQbWMupyVPlpC_KLs1pZ5-69aDTM,14850
8
8
  fastmcp/client/__init__.py,sha256=BXO9NUhntZ5GnUACfaRCzDJ5IzxqFJs8qKG-CRMSco4,490
9
9
  fastmcp/client/base.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
10
- fastmcp/client/client.py,sha256=1WxEaBqyAvYGhS6y_Xzei173Ufjd4rZ_REfbNFNWJnE,8033
10
+ fastmcp/client/client.py,sha256=XXpN28epV9N5w-keQSDPBCdLFtZ5EPK2msxuQ6PxTmo,7732
11
11
  fastmcp/client/roots.py,sha256=IxI_bHwHTmg6c2H-s1av1ZgrRnNDieHtYwdGFbzXT5c,2471
12
12
  fastmcp/client/sampling.py,sha256=WdRhIZbWv54rXYI8lWHv0thXmGCloZYPFpwJK9El_sQ,1613
13
- fastmcp/client/transports.py,sha256=lEE1PbqbntbFFKlVbrJtOpWzZa6N0_d1UXwixLgcpg0,15474
13
+ fastmcp/client/transports.py,sha256=7dJUQemdxj6UHNThizPzSJbHTGiJHlM77vLf4X9g11M,15491
14
+ fastmcp/contrib/README.md,sha256=rKknYSI1T192UvSszqwwDlQ2eYQpxywrNTLoj177SYU,878
15
+ fastmcp/contrib/bulk_tool_caller/README.md,sha256=5aUUY1TSFKtz1pvTLSDqkUCkGkuqMfMZNsLeaNqEgAc,1960
16
+ fastmcp/contrib/bulk_tool_caller/__init__.py,sha256=xvGSSaUXTQrc31erBoi1Gh7BikgOliETDiYVTP3rLxY,75
17
+ fastmcp/contrib/bulk_tool_caller/bulk_tool_caller.py,sha256=BlZnZRY_l4X5VnrIeMWzmQM2lRsQPMcnIafKIl4GXxE,4216
18
+ fastmcp/contrib/bulk_tool_caller/example.py,sha256=3RdsU2KrRwYZHEdVAmHOGJsO3ZJBxSaqz8BTznkPg7Y,321
19
+ fastmcp/contrib/mcp_mixin/README.md,sha256=9DDTJXWkA3yv1fp5V58gofmARPQ2xWDhblYGvUhKpDQ,1689
20
+ fastmcp/contrib/mcp_mixin/__init__.py,sha256=aw9IQ1ssNjCgws4ZNt8bkdpossAAGVAwwjBpMp9O5ZQ,153
21
+ fastmcp/contrib/mcp_mixin/example.py,sha256=GnunkXmtG5hLLTUsM8aW5ZURU52Z8vI4tNLl-fK7Dg0,1228
22
+ fastmcp/contrib/mcp_mixin/mcp_mixin.py,sha256=cfIRbnSxsVzglTD-auyTE0izVQeHP7Oz18qzYoBZJgg,7899
14
23
  fastmcp/prompts/__init__.py,sha256=LtPAv2JKIu54AwUd3iwv-HUd4DPcwgEqy6itEd3BH_E,194
15
24
  fastmcp/prompts/prompt.py,sha256=mQ6iRnt7J8oKBUhlgPDYXnIzwwDNWCk4heTqCmv1sco,6622
16
25
  fastmcp/prompts/prompt_manager.py,sha256=tMob9a-igjuzf6oTPLPGidFpJdg5JaPJVlYgyNkiCbE,2901
17
26
  fastmcp/resources/__init__.py,sha256=t0x1j8lc74rjUKtXe9H5Gs4fpQt82K4NgBK6Y7A0xTg,467
18
27
  fastmcp/resources/resource.py,sha256=5FN2a7dpNwf7FSEYTNvQvkTxtodu1OPxSlJL-U-8yrM,2413
19
28
  fastmcp/resources/resource_manager.py,sha256=_0itubfjYvfkA_wXKa4DQN5YpE7ejXhsE1hdt7m8XwU,9072
20
- fastmcp/resources/template.py,sha256=Xed7mmCNHUPG2lR9YOZ2MJ1jLiHP_Cp8Osms0b69ExM,5761
29
+ fastmcp/resources/template.py,sha256=PlC-fSGbQWJcFgM-fFgW-Xq8XwN3xsI68ivYcrk690E,5825
21
30
  fastmcp/resources/types.py,sha256=tigil7z-SUJMakGXzDLIGSqTepPrAsRpwqwtBA4yoUY,6168
22
31
  fastmcp/server/__init__.py,sha256=pdkghG11VLMZiluQ-4_rl2JK1LMWmV003m9dDRUN8W4,92
23
32
  fastmcp/server/context.py,sha256=s1885AZRipKB3VltfaO3VEtMxGefKs8fdZByj-4tbNI,7120
24
33
  fastmcp/server/openapi.py,sha256=DVdUfs-rbBF_CIlxrI6HJ5aYbzuyDqGLAhT1TeyxwFc,22424
25
- fastmcp/server/proxy.py,sha256=in-ZGwd7I8h7fITKMyHXaJRqODudn7MXsG0hVv9M0rA,8580
34
+ fastmcp/server/proxy.py,sha256=JHbxnOKbxyD5Jg2M_zSlNGKVBSZ5NUlVhQoKf442wxo,9619
26
35
  fastmcp/server/server.py,sha256=PFhnwa24diSKCz8KO39q43yuSHSbqYrzgnSspc-SPfg,31721
27
36
  fastmcp/tools/__init__.py,sha256=ocw-SFTtN6vQ8fgnlF8iNAOflRmh79xS1xdO0Bc3QPE,96
28
- fastmcp/tools/tool.py,sha256=FGihp_hzKLj4hK7EdHNUwe8o3NMzCngw4ftMmL_X4XI,5797
37
+ fastmcp/tools/tool.py,sha256=hAdeQaJ-1AgPyVZPvCQKYFkK0opccJWa39xWGFAWlzA,5975
29
38
  fastmcp/tools/tool_manager.py,sha256=hClv7fwj0cQSSwW0i-Swt7xiVqR4T9LVmr1Tp704nW4,3283
30
39
  fastmcp/utilities/__init__.py,sha256=-imJ8S-rXmbXMWeDamldP-dHDqAPg_wwmPVz-LNX14E,31
31
40
  fastmcp/utilities/decorators.py,sha256=AjhjsetQZF4YOPV5MTZmIxO21iFp_4fDIS3O2_KNCEg,2990
@@ -33,8 +42,8 @@ fastmcp/utilities/func_metadata.py,sha256=uh-u3gAjLD4kCcGf0ZkZZwBTTl-84JuANZTnDq
33
42
  fastmcp/utilities/logging.py,sha256=zav8pnFxG_fvGJHUV2XpobmT9WVrmv1mlQBSCz-CPx4,1159
34
43
  fastmcp/utilities/openapi.py,sha256=PrH3usbTblaVC6jIH1UGiPEfgB2sSCLj33zA5dH7o_s,45193
35
44
  fastmcp/utilities/types.py,sha256=m2rPYMzO-ZFvvZ46N-1-Xqyw693K7yq9Z2xR4pVELyk,2091
36
- fastmcp-2.2.1.dist-info/METADATA,sha256=DSlO410ImcFq2nxjE-BfQaqzXLkkThKE9yLe-pTKA1k,27760
37
- fastmcp-2.2.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
38
- fastmcp-2.2.1.dist-info/entry_points.txt,sha256=ff8bMtKX1JvXyurMibAacMSKbJEPmac9ffAKU9mLnM8,44
39
- fastmcp-2.2.1.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
40
- fastmcp-2.2.1.dist-info/RECORD,,
45
+ fastmcp-2.2.3.dist-info/METADATA,sha256=dC6bbGM3xckA7dpYhpD3zQUyxSpTqLV0DppozXl5pjU,27771
46
+ fastmcp-2.2.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
47
+ fastmcp-2.2.3.dist-info/entry_points.txt,sha256=ff8bMtKX1JvXyurMibAacMSKbJEPmac9ffAKU9mLnM8,44
48
+ fastmcp-2.2.3.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
49
+ fastmcp-2.2.3.dist-info/RECORD,,