fastmcp 2.1.1__py3-none-any.whl → 2.2.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/cli/cli.py +32 -0
- fastmcp/client/client.py +16 -15
- fastmcp/client/transports.py +28 -7
- fastmcp/exceptions.py +8 -0
- fastmcp/prompts/prompt.py +20 -9
- fastmcp/prompts/prompt_manager.py +37 -45
- fastmcp/resources/resource.py +19 -8
- fastmcp/resources/resource_manager.py +83 -115
- fastmcp/resources/template.py +82 -17
- fastmcp/server/openapi.py +10 -16
- fastmcp/server/proxy.py +102 -76
- fastmcp/server/server.py +319 -256
- fastmcp/settings.py +8 -11
- fastmcp/tools/tool.py +66 -14
- fastmcp/tools/tool_manager.py +46 -44
- fastmcp/utilities/logging.py +14 -6
- fastmcp/utilities/openapi.py +0 -87
- {fastmcp-2.1.1.dist-info → fastmcp-2.2.0.dist-info}/METADATA +20 -7
- fastmcp-2.2.0.dist-info/RECORD +40 -0
- fastmcp-2.1.1.dist-info/RECORD +0 -40
- {fastmcp-2.1.1.dist-info → fastmcp-2.2.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.1.1.dist-info → fastmcp-2.2.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.1.1.dist-info → fastmcp-2.2.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/settings.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations as _annotations
|
|
2
2
|
|
|
3
|
-
from enum import Enum
|
|
4
3
|
from typing import TYPE_CHECKING, Literal
|
|
5
4
|
|
|
6
5
|
from pydantic import Field
|
|
@@ -11,12 +10,7 @@ if TYPE_CHECKING:
|
|
|
11
10
|
|
|
12
11
|
LOG_LEVEL = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
class DuplicateBehavior(Enum):
|
|
16
|
-
WARN = "warn"
|
|
17
|
-
ERROR = "error"
|
|
18
|
-
REPLACE = "replace"
|
|
19
|
-
IGNORE = "ignore"
|
|
13
|
+
DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
|
|
20
14
|
|
|
21
15
|
|
|
22
16
|
class Settings(BaseSettings):
|
|
@@ -48,26 +42,29 @@ class ServerSettings(BaseSettings):
|
|
|
48
42
|
log_level: LOG_LEVEL = Field(default_factory=lambda: Settings().log_level)
|
|
49
43
|
|
|
50
44
|
# HTTP settings
|
|
51
|
-
host: str = "
|
|
45
|
+
host: str = "127.0.0.1"
|
|
52
46
|
port: int = 8000
|
|
53
47
|
sse_path: str = "/sse"
|
|
54
48
|
message_path: str = "/messages/"
|
|
55
49
|
debug: bool = False
|
|
56
50
|
|
|
57
51
|
# resource settings
|
|
58
|
-
on_duplicate_resources: DuplicateBehavior =
|
|
52
|
+
on_duplicate_resources: DuplicateBehavior = "warn"
|
|
59
53
|
|
|
60
54
|
# tool settings
|
|
61
|
-
on_duplicate_tools: DuplicateBehavior =
|
|
55
|
+
on_duplicate_tools: DuplicateBehavior = "warn"
|
|
62
56
|
|
|
63
57
|
# prompt settings
|
|
64
|
-
on_duplicate_prompts: DuplicateBehavior =
|
|
58
|
+
on_duplicate_prompts: DuplicateBehavior = "warn"
|
|
65
59
|
|
|
66
60
|
dependencies: list[str] = Field(
|
|
67
61
|
default_factory=list,
|
|
68
62
|
description="List of dependencies to install in the server environment",
|
|
69
63
|
)
|
|
70
64
|
|
|
65
|
+
# cache settings (for checking mounted servers)
|
|
66
|
+
cache_expiration_seconds: float = 0
|
|
67
|
+
|
|
71
68
|
|
|
72
69
|
class ClientSettings(BaseSettings):
|
|
73
70
|
"""FastMCP client settings."""
|
fastmcp/tools/tool.py
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
1
|
+
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
|
+
import json
|
|
4
5
|
from collections.abc import Callable
|
|
5
6
|
from typing import TYPE_CHECKING, Annotated, Any
|
|
6
7
|
|
|
8
|
+
import pydantic_core
|
|
9
|
+
from mcp.types import EmbeddedResource, ImageContent, TextContent
|
|
10
|
+
from mcp.types import Tool as MCPTool
|
|
7
11
|
from pydantic import BaseModel, BeforeValidator, Field
|
|
8
|
-
from typing_extensions import Self
|
|
9
12
|
|
|
10
13
|
from fastmcp.exceptions import ToolError
|
|
11
14
|
from fastmcp.utilities.func_metadata import FuncMetadata, func_metadata
|
|
12
|
-
from fastmcp.utilities.types import _convert_set_defaults
|
|
15
|
+
from fastmcp.utilities.types import Image, _convert_set_defaults
|
|
13
16
|
|
|
14
17
|
if TYPE_CHECKING:
|
|
15
18
|
from mcp.server.session import ServerSessionT
|
|
@@ -58,7 +61,7 @@ class Tool(BaseModel):
|
|
|
58
61
|
is_async = inspect.iscoroutinefunction(fn)
|
|
59
62
|
|
|
60
63
|
if context_kwarg is None:
|
|
61
|
-
if
|
|
64
|
+
if inspect.ismethod(fn) and hasattr(fn, "__func__"):
|
|
62
65
|
sig = inspect.signature(fn.__func__)
|
|
63
66
|
else:
|
|
64
67
|
sig = inspect.signature(fn)
|
|
@@ -67,14 +70,16 @@ class Tool(BaseModel):
|
|
|
67
70
|
context_kwarg = param_name
|
|
68
71
|
break
|
|
69
72
|
|
|
73
|
+
# Use callable typing to ensure fn is treated as a callable despite being a classmethod
|
|
74
|
+
fn_callable: Callable[..., Any] = fn
|
|
70
75
|
func_arg_metadata = func_metadata(
|
|
71
|
-
|
|
76
|
+
fn_callable,
|
|
72
77
|
skip_names=[context_kwarg] if context_kwarg is not None else [],
|
|
73
78
|
)
|
|
74
79
|
parameters = func_arg_metadata.arg_model.model_json_schema()
|
|
75
80
|
|
|
76
81
|
return cls(
|
|
77
|
-
fn=
|
|
82
|
+
fn=fn_callable,
|
|
78
83
|
name=func_name,
|
|
79
84
|
description=func_doc,
|
|
80
85
|
parameters=parameters,
|
|
@@ -88,10 +93,10 @@ class Tool(BaseModel):
|
|
|
88
93
|
self,
|
|
89
94
|
arguments: dict[str, Any],
|
|
90
95
|
context: Context[ServerSessionT, LifespanContextT] | None = None,
|
|
91
|
-
) ->
|
|
96
|
+
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
92
97
|
"""Run the tool with arguments."""
|
|
93
98
|
try:
|
|
94
|
-
|
|
99
|
+
result = await self.fn_metadata.call_fn_with_arg_validation(
|
|
95
100
|
self.fn,
|
|
96
101
|
self.is_async,
|
|
97
102
|
arguments,
|
|
@@ -99,17 +104,64 @@ class Tool(BaseModel):
|
|
|
99
104
|
if self.context_kwarg is not None
|
|
100
105
|
else None,
|
|
101
106
|
)
|
|
107
|
+
return _convert_to_content(result)
|
|
102
108
|
except Exception as e:
|
|
103
109
|
raise ToolError(f"Error executing tool {self.name}: {e}") from e
|
|
104
110
|
|
|
105
|
-
def
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
+
def to_mcp_tool(self, **overrides: Any) -> MCPTool:
|
|
112
|
+
kwargs = {
|
|
113
|
+
"name": self.name,
|
|
114
|
+
"description": self.description,
|
|
115
|
+
"inputSchema": self.parameters,
|
|
116
|
+
}
|
|
117
|
+
return MCPTool(**kwargs | overrides)
|
|
111
118
|
|
|
112
119
|
def __eq__(self, other: object) -> bool:
|
|
113
120
|
if not isinstance(other, Tool):
|
|
114
121
|
return False
|
|
115
122
|
return self.model_dump() == other.model_dump()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _convert_to_content(
|
|
126
|
+
result: Any,
|
|
127
|
+
_process_as_single_item: bool = False,
|
|
128
|
+
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
129
|
+
"""Convert a result to a sequence of content objects."""
|
|
130
|
+
if result is None:
|
|
131
|
+
return []
|
|
132
|
+
|
|
133
|
+
if isinstance(result, TextContent | ImageContent | EmbeddedResource):
|
|
134
|
+
return [result]
|
|
135
|
+
|
|
136
|
+
if isinstance(result, Image):
|
|
137
|
+
return [result.to_image_content()]
|
|
138
|
+
|
|
139
|
+
if isinstance(result, list | tuple) and not _process_as_single_item:
|
|
140
|
+
# if the result is a list, then it could either be a list of MCP types,
|
|
141
|
+
# or a "regular" list that the tool is returning, or a mix of both.
|
|
142
|
+
#
|
|
143
|
+
# so we extract all the MCP types / images and convert them as individual content elements,
|
|
144
|
+
# and aggregate the rest as a single content element
|
|
145
|
+
|
|
146
|
+
mcp_types = []
|
|
147
|
+
other_content = []
|
|
148
|
+
|
|
149
|
+
for item in result:
|
|
150
|
+
if isinstance(item, TextContent | ImageContent | EmbeddedResource | Image):
|
|
151
|
+
mcp_types.append(_convert_to_content(item)[0])
|
|
152
|
+
else:
|
|
153
|
+
other_content.append(item)
|
|
154
|
+
if other_content:
|
|
155
|
+
other_content = _convert_to_content(
|
|
156
|
+
other_content, _process_as_single_item=True
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return other_content + mcp_types
|
|
160
|
+
|
|
161
|
+
if not isinstance(result, str):
|
|
162
|
+
try:
|
|
163
|
+
result = json.dumps(pydantic_core.to_jsonable_python(result))
|
|
164
|
+
except Exception:
|
|
165
|
+
result = str(result)
|
|
166
|
+
|
|
167
|
+
return [TextContent(type="text", text=result)]
|
fastmcp/tools/tool_manager.py
CHANGED
|
@@ -4,8 +4,9 @@ from collections.abc import Callable
|
|
|
4
4
|
from typing import TYPE_CHECKING, Any
|
|
5
5
|
|
|
6
6
|
from mcp.shared.context import LifespanContextT
|
|
7
|
+
from mcp.types import EmbeddedResource, ImageContent, TextContent
|
|
7
8
|
|
|
8
|
-
from fastmcp.exceptions import
|
|
9
|
+
from fastmcp.exceptions import NotFoundError
|
|
9
10
|
from fastmcp.settings import DuplicateBehavior
|
|
10
11
|
from fastmcp.tools.tool import Tool
|
|
11
12
|
from fastmcp.utilities.logging import get_logger
|
|
@@ -21,17 +22,38 @@ logger = get_logger(__name__)
|
|
|
21
22
|
class ToolManager:
|
|
22
23
|
"""Manages FastMCP tools."""
|
|
23
24
|
|
|
24
|
-
def __init__(self, duplicate_behavior: DuplicateBehavior =
|
|
25
|
+
def __init__(self, duplicate_behavior: DuplicateBehavior | None = None):
|
|
25
26
|
self._tools: dict[str, Tool] = {}
|
|
27
|
+
|
|
28
|
+
# Default to "warn" if None is provided
|
|
29
|
+
if duplicate_behavior is None:
|
|
30
|
+
duplicate_behavior = "warn"
|
|
31
|
+
|
|
32
|
+
if duplicate_behavior not in DuplicateBehavior.__args__:
|
|
33
|
+
raise ValueError(
|
|
34
|
+
f"Invalid duplicate_behavior: {duplicate_behavior}. "
|
|
35
|
+
f"Must be one of: {', '.join(DuplicateBehavior.__args__)}"
|
|
36
|
+
)
|
|
37
|
+
|
|
26
38
|
self.duplicate_behavior = duplicate_behavior
|
|
27
39
|
|
|
28
|
-
def
|
|
29
|
-
"""
|
|
30
|
-
return self._tools
|
|
40
|
+
def has_tool(self, key: str) -> bool:
|
|
41
|
+
"""Check if a tool exists."""
|
|
42
|
+
return key in self._tools
|
|
43
|
+
|
|
44
|
+
def get_tool(self, key: str) -> Tool:
|
|
45
|
+
"""Get tool by key."""
|
|
46
|
+
if key in self._tools:
|
|
47
|
+
return self._tools[key]
|
|
48
|
+
raise NotFoundError(f"Unknown tool: {key}")
|
|
49
|
+
|
|
50
|
+
def get_tools(self) -> dict[str, Tool]:
|
|
51
|
+
"""Get all registered tools, indexed by registered key."""
|
|
52
|
+
return self._tools
|
|
31
53
|
|
|
32
54
|
def list_tools(self) -> list[Tool]:
|
|
33
55
|
"""List all registered tools."""
|
|
34
|
-
return list(self.
|
|
56
|
+
return list(self.get_tools().values())
|
|
35
57
|
|
|
36
58
|
def add_tool_from_fn(
|
|
37
59
|
self,
|
|
@@ -44,53 +66,33 @@ class ToolManager:
|
|
|
44
66
|
tool = Tool.from_function(fn, name=name, description=description, tags=tags)
|
|
45
67
|
return self.add_tool(tool)
|
|
46
68
|
|
|
47
|
-
def add_tool(self, tool: Tool) -> Tool:
|
|
69
|
+
def add_tool(self, tool: Tool, key: str | None = None) -> Tool:
|
|
48
70
|
"""Register a tool with the server."""
|
|
49
|
-
|
|
71
|
+
key = key or tool.name
|
|
72
|
+
existing = self._tools.get(key)
|
|
50
73
|
if existing:
|
|
51
|
-
if self.duplicate_behavior ==
|
|
52
|
-
logger.warning(f"Tool already exists: {
|
|
53
|
-
self._tools[
|
|
54
|
-
elif self.duplicate_behavior ==
|
|
55
|
-
self._tools[
|
|
56
|
-
elif self.duplicate_behavior ==
|
|
57
|
-
raise ValueError(f"Tool already exists: {
|
|
58
|
-
elif self.duplicate_behavior ==
|
|
59
|
-
|
|
60
|
-
|
|
74
|
+
if self.duplicate_behavior == "warn":
|
|
75
|
+
logger.warning(f"Tool already exists: {key}")
|
|
76
|
+
self._tools[key] = tool
|
|
77
|
+
elif self.duplicate_behavior == "replace":
|
|
78
|
+
self._tools[key] = tool
|
|
79
|
+
elif self.duplicate_behavior == "error":
|
|
80
|
+
raise ValueError(f"Tool already exists: {key}")
|
|
81
|
+
elif self.duplicate_behavior == "ignore":
|
|
82
|
+
return existing
|
|
83
|
+
else:
|
|
84
|
+
self._tools[key] = tool
|
|
61
85
|
return tool
|
|
62
86
|
|
|
63
87
|
async def call_tool(
|
|
64
88
|
self,
|
|
65
|
-
|
|
89
|
+
key: str,
|
|
66
90
|
arguments: dict[str, Any],
|
|
67
91
|
context: Context[ServerSessionT, LifespanContextT] | None = None,
|
|
68
|
-
) ->
|
|
92
|
+
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
69
93
|
"""Call a tool by name with arguments."""
|
|
70
|
-
tool = self.get_tool(
|
|
94
|
+
tool = self.get_tool(key)
|
|
71
95
|
if not tool:
|
|
72
|
-
raise
|
|
96
|
+
raise NotFoundError(f"Unknown tool: {key}")
|
|
73
97
|
|
|
74
98
|
return await tool.run(arguments, context=context)
|
|
75
|
-
|
|
76
|
-
def import_tools(
|
|
77
|
-
self, tool_manager: ToolManager, prefix: str | None = None
|
|
78
|
-
) -> None:
|
|
79
|
-
"""
|
|
80
|
-
Import all tools from another ToolManager with prefixed names.
|
|
81
|
-
|
|
82
|
-
Args:
|
|
83
|
-
tool_manager: Another ToolManager instance to import tools from
|
|
84
|
-
prefix: Prefix to add to tool names, including the delimiter.
|
|
85
|
-
The resulting tool name will be in the format "{prefix}{original_name}"
|
|
86
|
-
if prefix is provided, otherwise the original name is used.
|
|
87
|
-
For example, with prefix "weather/" and tool "forecast",
|
|
88
|
-
the imported tool would be available as "weather/forecast"
|
|
89
|
-
"""
|
|
90
|
-
for name, tool in tool_manager._tools.items():
|
|
91
|
-
prefixed_name = f"{prefix}{name}" if prefix else name
|
|
92
|
-
|
|
93
|
-
new_tool = tool.copy(updates=dict(name=prefixed_name))
|
|
94
|
-
# Store the copied tool
|
|
95
|
-
self.add_tool(new_tool)
|
|
96
|
-
logger.debug(f'Imported tool "{name}" as "{prefixed_name}"')
|
fastmcp/utilities/logging.py
CHANGED
|
@@ -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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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)
|
fastmcp/utilities/openapi.py
CHANGED
|
@@ -150,93 +150,6 @@ def _resolve_ref(
|
|
|
150
150
|
return item
|
|
151
151
|
|
|
152
152
|
|
|
153
|
-
def _extract_schema_as_dict(
|
|
154
|
-
schema_obj: Schema | Reference, openapi: OpenAPI
|
|
155
|
-
) -> JsonSchema:
|
|
156
|
-
"""Resolves a schema/reference and returns it as a dictionary."""
|
|
157
|
-
resolved_schema = _resolve_ref(schema_obj, openapi)
|
|
158
|
-
if isinstance(resolved_schema, Schema):
|
|
159
|
-
# Using exclude_none=True might be better than exclude_unset sometimes
|
|
160
|
-
return resolved_schema.model_dump(mode="json", by_alias=True, exclude_none=True)
|
|
161
|
-
elif isinstance(resolved_schema, dict):
|
|
162
|
-
logger.warning(
|
|
163
|
-
"Resolved schema reference resulted in a dict, not a Schema model."
|
|
164
|
-
)
|
|
165
|
-
return resolved_schema
|
|
166
|
-
else:
|
|
167
|
-
ref_str = getattr(schema_obj, "ref", "unknown")
|
|
168
|
-
logger.warning(
|
|
169
|
-
f"Expected Schema after resolving ref '{ref_str}', got {type(resolved_schema)}. Returning empty dict."
|
|
170
|
-
)
|
|
171
|
-
return {}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def _convert_to_parameter_location(param_in: str) -> ParameterLocation:
|
|
175
|
-
"""Convert string parameter location to our ParameterLocation type."""
|
|
176
|
-
if param_in == "path":
|
|
177
|
-
return "path"
|
|
178
|
-
elif param_in == "query":
|
|
179
|
-
return "query"
|
|
180
|
-
elif param_in == "header":
|
|
181
|
-
return "header"
|
|
182
|
-
elif param_in == "cookie":
|
|
183
|
-
return "cookie"
|
|
184
|
-
else:
|
|
185
|
-
logger.warning(f"Unknown parameter location: {param_in}, defaulting to 'query'")
|
|
186
|
-
return "query"
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
def _extract_responses(
|
|
190
|
-
operation_responses: dict[str, Response | Reference] | None,
|
|
191
|
-
openapi: OpenAPI,
|
|
192
|
-
) -> dict[str, ResponseInfo]:
|
|
193
|
-
"""Extracts and resolves response information for an operation."""
|
|
194
|
-
extracted_responses: dict[str, ResponseInfo] = {}
|
|
195
|
-
if not operation_responses:
|
|
196
|
-
return extracted_responses
|
|
197
|
-
|
|
198
|
-
for status_code, resp_or_ref in operation_responses.items():
|
|
199
|
-
try:
|
|
200
|
-
response = cast(Response, _resolve_ref(resp_or_ref, openapi))
|
|
201
|
-
if not isinstance(response, Response):
|
|
202
|
-
ref_str = getattr(resp_or_ref, "ref", "unknown")
|
|
203
|
-
logger.warning(
|
|
204
|
-
f"Expected Response after resolving ref '{ref_str}' for status code {status_code}, got {type(response)}. Skipping."
|
|
205
|
-
)
|
|
206
|
-
continue
|
|
207
|
-
|
|
208
|
-
content_schemas: dict[str, JsonSchema] = {}
|
|
209
|
-
if response.content:
|
|
210
|
-
for media_type_str, media_type_obj in response.content.items():
|
|
211
|
-
if (
|
|
212
|
-
isinstance(media_type_obj, MediaType)
|
|
213
|
-
and media_type_obj.media_type_schema
|
|
214
|
-
):
|
|
215
|
-
try:
|
|
216
|
-
schema_dict = _extract_schema_as_dict(
|
|
217
|
-
media_type_obj.media_type_schema, openapi
|
|
218
|
-
)
|
|
219
|
-
content_schemas[media_type_str] = schema_dict
|
|
220
|
-
except ValueError as schema_err:
|
|
221
|
-
logger.error(
|
|
222
|
-
f"Failed to extract schema for media type '{media_type_str}' in response {status_code}: {schema_err}"
|
|
223
|
-
)
|
|
224
|
-
|
|
225
|
-
resp_info = ResponseInfo(
|
|
226
|
-
description=response.description, content_schema=content_schemas
|
|
227
|
-
)
|
|
228
|
-
extracted_responses[str(status_code)] = resp_info
|
|
229
|
-
|
|
230
|
-
except (ValidationError, ValueError, AttributeError) as e:
|
|
231
|
-
ref_name = getattr(resp_or_ref, "ref", "unknown")
|
|
232
|
-
logger.error(
|
|
233
|
-
f"Failed to extract response for status code {status_code} (ref: '{ref_name}'): {e}",
|
|
234
|
-
exc_info=False,
|
|
235
|
-
)
|
|
236
|
-
|
|
237
|
-
return extracted_responses
|
|
238
|
-
|
|
239
|
-
|
|
240
153
|
# --- Main Parsing Function ---
|
|
241
154
|
# (No changes needed in the main loop logic, only in the helpers it calls)
|
|
242
155
|
def parse_openapi_to_http_routes(openapi_dict: dict[str, Any]) -> list[HTTPRoute]:
|
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastmcp
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: The fast, Pythonic way to build MCP servers.
|
|
5
|
+
Project-URL: Homepage, https://gofastmcp.com
|
|
6
|
+
Project-URL: Repository, https://github.com/jlowin/fastmcp
|
|
7
|
+
Project-URL: Documentation, https://gofastmcp.com
|
|
5
8
|
Author: Jeremiah Lowin
|
|
6
|
-
License: Apache-2.0
|
|
9
|
+
License-Expression: Apache-2.0
|
|
7
10
|
License-File: LICENSE
|
|
11
|
+
Keywords: agent,fastmcp,llm,mcp,mcp client,mcp server,model context protocol
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Classifier: Typing :: Typed
|
|
8
19
|
Requires-Python: >=3.10
|
|
9
20
|
Requires-Dist: dotenv>=0.9.9
|
|
10
|
-
Requires-Dist:
|
|
21
|
+
Requires-Dist: exceptiongroup>=1.2.2
|
|
22
|
+
Requires-Dist: httpx>=0.28.1
|
|
11
23
|
Requires-Dist: mcp<2.0.0,>=1.6.0
|
|
12
24
|
Requires-Dist: openapi-pydantic>=0.5.1
|
|
13
25
|
Requires-Dist: rich>=13.9.4
|
|
@@ -26,6 +38,7 @@ Description-Content-Type: text/markdown
|
|
|
26
38
|
[](https://github.com/jlowin/fastmcp/actions/workflows/run-tests.yml)
|
|
27
39
|
[](https://github.com/jlowin/fastmcp/blob/main/LICENSE)
|
|
28
40
|
|
|
41
|
+
<a href="https://trendshift.io/repositories/13266" target="_blank"><img src="https://trendshift.io/api/badge/repositories/13266" alt="jlowin%2Ffastmcp | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
|
29
42
|
</div>
|
|
30
43
|
|
|
31
44
|
The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) is a new, standardized way to provide context and tools to your LLMs, and FastMCP makes building MCP servers and clients simple and intuitive. Create tools, expose resources, define prompts, and connect components with clean, Pythonic code.
|
|
@@ -109,7 +122,7 @@ FastMCP provides a high-level, Pythonic interface for building and interacting w
|
|
|
109
122
|
|
|
110
123
|
## Why FastMCP?
|
|
111
124
|
|
|
112
|
-
The MCP protocol is powerful but implementing it involves a lot of boilerplate - server setup, protocol handlers, content types, error management. FastMCP handles all the complex protocol details and server management, so you can focus on building great tools. It
|
|
125
|
+
The MCP protocol is powerful but implementing it involves a lot of boilerplate - server setup, protocol handlers, content types, error management. FastMCP handles all the complex protocol details and server management, so you can focus on building great tools. It's designed to be high-level and Pythonic; in most cases, decorating a function is all you need.
|
|
113
126
|
|
|
114
127
|
FastMCP aims to be:
|
|
115
128
|
|
|
@@ -568,13 +581,13 @@ proxy_client = Client(
|
|
|
568
581
|
)
|
|
569
582
|
|
|
570
583
|
# Create a proxy server that connects to the client and exposes its capabilities
|
|
571
|
-
proxy = FastMCP.
|
|
584
|
+
proxy = FastMCP.from_client(proxy_client, name="Stdio-to-SSE Proxy")
|
|
572
585
|
|
|
573
586
|
if __name__ == "__main__":
|
|
574
587
|
proxy.run(transport='sse')
|
|
575
588
|
```
|
|
576
589
|
|
|
577
|
-
`FastMCP.
|
|
590
|
+
`FastMCP.from_client` is a class method that connects to the target, discovers its capabilities, and dynamically builds the proxy server instance.
|
|
578
591
|
|
|
579
592
|
|
|
580
593
|
|
|
@@ -787,4 +800,4 @@ We use `ruff` via `pre-commit`.
|
|
|
787
800
|
|
|
788
801
|
Please open an issue or discussion for questions or suggestions!
|
|
789
802
|
|
|
790
|
-
</details>
|
|
803
|
+
</details>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
fastmcp/__init__.py,sha256=2bwhjiyLJisyobp1O9tVYMjriHZAx_f4bIKJYOL-Rpk,399
|
|
2
|
+
fastmcp/exceptions.py,sha256=QKVHbftoZp4YZQ2NxA-t1SjztqspFdX95YTFOAmr5EE,640
|
|
3
|
+
fastmcp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
fastmcp/settings.py,sha256=VCjc-3011pKRYjt2h9rZ68XhVEekbpyLyVUREVBTSrg,1955
|
|
5
|
+
fastmcp/cli/__init__.py,sha256=Ii284TNoG5lxTP40ETMGhHEq3lQZWxu9m9JuU57kUpQ,87
|
|
6
|
+
fastmcp/cli/claude.py,sha256=IAlcZ4qZKBBj09jZUMEx7EANZE_IR3vcu7zOBJmMOuU,4567
|
|
7
|
+
fastmcp/cli/cli.py,sha256=vdnJi_zz4ZYoPxp9xlJbh6RlGogaFY3icaOPzO_xsLE,14874
|
|
8
|
+
fastmcp/client/__init__.py,sha256=BXO9NUhntZ5GnUACfaRCzDJ5IzxqFJs8qKG-CRMSco4,490
|
|
9
|
+
fastmcp/client/base.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
10
|
+
fastmcp/client/client.py,sha256=1WxEaBqyAvYGhS6y_Xzei173Ufjd4rZ_REfbNFNWJnE,8033
|
|
11
|
+
fastmcp/client/roots.py,sha256=IxI_bHwHTmg6c2H-s1av1ZgrRnNDieHtYwdGFbzXT5c,2471
|
|
12
|
+
fastmcp/client/sampling.py,sha256=WdRhIZbWv54rXYI8lWHv0thXmGCloZYPFpwJK9El_sQ,1613
|
|
13
|
+
fastmcp/client/transports.py,sha256=lEE1PbqbntbFFKlVbrJtOpWzZa6N0_d1UXwixLgcpg0,15474
|
|
14
|
+
fastmcp/prompts/__init__.py,sha256=LtPAv2JKIu54AwUd3iwv-HUd4DPcwgEqy6itEd3BH_E,194
|
|
15
|
+
fastmcp/prompts/prompt.py,sha256=mQ6iRnt7J8oKBUhlgPDYXnIzwwDNWCk4heTqCmv1sco,6622
|
|
16
|
+
fastmcp/prompts/prompt_manager.py,sha256=tMob9a-igjuzf6oTPLPGidFpJdg5JaPJVlYgyNkiCbE,2901
|
|
17
|
+
fastmcp/resources/__init__.py,sha256=t0x1j8lc74rjUKtXe9H5Gs4fpQt82K4NgBK6Y7A0xTg,467
|
|
18
|
+
fastmcp/resources/resource.py,sha256=5FN2a7dpNwf7FSEYTNvQvkTxtodu1OPxSlJL-U-8yrM,2413
|
|
19
|
+
fastmcp/resources/resource_manager.py,sha256=_0itubfjYvfkA_wXKa4DQN5YpE7ejXhsE1hdt7m8XwU,9072
|
|
20
|
+
fastmcp/resources/template.py,sha256=Xed7mmCNHUPG2lR9YOZ2MJ1jLiHP_Cp8Osms0b69ExM,5761
|
|
21
|
+
fastmcp/resources/types.py,sha256=tigil7z-SUJMakGXzDLIGSqTepPrAsRpwqwtBA4yoUY,6168
|
|
22
|
+
fastmcp/server/__init__.py,sha256=pdkghG11VLMZiluQ-4_rl2JK1LMWmV003m9dDRUN8W4,92
|
|
23
|
+
fastmcp/server/context.py,sha256=s1885AZRipKB3VltfaO3VEtMxGefKs8fdZByj-4tbNI,7120
|
|
24
|
+
fastmcp/server/openapi.py,sha256=DVdUfs-rbBF_CIlxrI6HJ5aYbzuyDqGLAhT1TeyxwFc,22424
|
|
25
|
+
fastmcp/server/proxy.py,sha256=in-ZGwd7I8h7fITKMyHXaJRqODudn7MXsG0hVv9M0rA,8580
|
|
26
|
+
fastmcp/server/server.py,sha256=izKw5m1nuabaFg4DeC26DYqytEq1GkqUMiuMoNVTUYI,31681
|
|
27
|
+
fastmcp/tools/__init__.py,sha256=ocw-SFTtN6vQ8fgnlF8iNAOflRmh79xS1xdO0Bc3QPE,96
|
|
28
|
+
fastmcp/tools/tool.py,sha256=FGihp_hzKLj4hK7EdHNUwe8o3NMzCngw4ftMmL_X4XI,5797
|
|
29
|
+
fastmcp/tools/tool_manager.py,sha256=hClv7fwj0cQSSwW0i-Swt7xiVqR4T9LVmr1Tp704nW4,3283
|
|
30
|
+
fastmcp/utilities/__init__.py,sha256=-imJ8S-rXmbXMWeDamldP-dHDqAPg_wwmPVz-LNX14E,31
|
|
31
|
+
fastmcp/utilities/decorators.py,sha256=AjhjsetQZF4YOPV5MTZmIxO21iFp_4fDIS3O2_KNCEg,2990
|
|
32
|
+
fastmcp/utilities/func_metadata.py,sha256=uh-u3gAjLD4kCcGf0ZkZZwBTTl-84JuANZTnDqP5ztI,7841
|
|
33
|
+
fastmcp/utilities/logging.py,sha256=zav8pnFxG_fvGJHUV2XpobmT9WVrmv1mlQBSCz-CPx4,1159
|
|
34
|
+
fastmcp/utilities/openapi.py,sha256=PrH3usbTblaVC6jIH1UGiPEfgB2sSCLj33zA5dH7o_s,45193
|
|
35
|
+
fastmcp/utilities/types.py,sha256=m2rPYMzO-ZFvvZ46N-1-Xqyw693K7yq9Z2xR4pVELyk,2091
|
|
36
|
+
fastmcp-2.2.0.dist-info/METADATA,sha256=MheLzFNF_LxSDXigYZ4MS5kswED3sFguTOACOe0RjUs,27497
|
|
37
|
+
fastmcp-2.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
38
|
+
fastmcp-2.2.0.dist-info/entry_points.txt,sha256=ff8bMtKX1JvXyurMibAacMSKbJEPmac9ffAKU9mLnM8,44
|
|
39
|
+
fastmcp-2.2.0.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
|
|
40
|
+
fastmcp-2.2.0.dist-info/RECORD,,
|
fastmcp-2.1.1.dist-info/RECORD
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
fastmcp/__init__.py,sha256=2bwhjiyLJisyobp1O9tVYMjriHZAx_f4bIKJYOL-Rpk,399
|
|
2
|
-
fastmcp/exceptions.py,sha256=9_KfANmugNdfZTCSZrohSeVmFmILZX5zz1LXI3wr_pw,508
|
|
3
|
-
fastmcp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
fastmcp/settings.py,sha256=r5LT5yAZmwS1Lppro_fzdrCUJ0otnAmgtjsNgDdQIvs,1980
|
|
5
|
-
fastmcp/cli/__init__.py,sha256=Ii284TNoG5lxTP40ETMGhHEq3lQZWxu9m9JuU57kUpQ,87
|
|
6
|
-
fastmcp/cli/claude.py,sha256=IAlcZ4qZKBBj09jZUMEx7EANZE_IR3vcu7zOBJmMOuU,4567
|
|
7
|
-
fastmcp/cli/cli.py,sha256=M3BAkCjtnoGbyorUgaiqmCVSQg7S7_uEXerY-pqnOhk,13997
|
|
8
|
-
fastmcp/client/__init__.py,sha256=BXO9NUhntZ5GnUACfaRCzDJ5IzxqFJs8qKG-CRMSco4,490
|
|
9
|
-
fastmcp/client/base.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
10
|
-
fastmcp/client/client.py,sha256=OMJ1FzrlcRRYKQg3DFxiQpaNDo4uYK3Acs23rxPyE-g,7941
|
|
11
|
-
fastmcp/client/roots.py,sha256=IxI_bHwHTmg6c2H-s1av1ZgrRnNDieHtYwdGFbzXT5c,2471
|
|
12
|
-
fastmcp/client/sampling.py,sha256=WdRhIZbWv54rXYI8lWHv0thXmGCloZYPFpwJK9El_sQ,1613
|
|
13
|
-
fastmcp/client/transports.py,sha256=WVChsDV1UF0I3reiefsT3dipIh-P_K262TXpucwH-YY,14602
|
|
14
|
-
fastmcp/prompts/__init__.py,sha256=LtPAv2JKIu54AwUd3iwv-HUd4DPcwgEqy6itEd3BH_E,194
|
|
15
|
-
fastmcp/prompts/prompt.py,sha256=wy5gHHiTeYDsDufrVlfJzej_A7lTT34WNI9jwsEaNn8,6268
|
|
16
|
-
fastmcp/prompts/prompt_manager.py,sha256=HGwg8vsx1TVh9hBBWwuDdLm-M-FzBU5oB6Xu8nZC524,3505
|
|
17
|
-
fastmcp/resources/__init__.py,sha256=t0x1j8lc74rjUKtXe9H5Gs4fpQt82K4NgBK6Y7A0xTg,467
|
|
18
|
-
fastmcp/resources/resource.py,sha256=MrfRdLA2FEglvRJP7KgduG7na_qgkBo-_iXTzRbil6c,2038
|
|
19
|
-
fastmcp/resources/resource_manager.py,sha256=fIre1GkXhOyWJNGM36qwYoZQC74aEznV4oXfLfCoKdw,10971
|
|
20
|
-
fastmcp/resources/template.py,sha256=Fpjb51_ktWFpS1aQ5CFCt1SFuPe6S7CPuyzQCz7c3Mg,3742
|
|
21
|
-
fastmcp/resources/types.py,sha256=tigil7z-SUJMakGXzDLIGSqTepPrAsRpwqwtBA4yoUY,6168
|
|
22
|
-
fastmcp/server/__init__.py,sha256=pdkghG11VLMZiluQ-4_rl2JK1LMWmV003m9dDRUN8W4,92
|
|
23
|
-
fastmcp/server/context.py,sha256=s1885AZRipKB3VltfaO3VEtMxGefKs8fdZByj-4tbNI,7120
|
|
24
|
-
fastmcp/server/openapi.py,sha256=J7HrAlRziaB2a6pwB0wStbjRJ1E5Lf818yMqD762s5U,22693
|
|
25
|
-
fastmcp/server/proxy.py,sha256=gYcoQFDIBraqWMOpWSsZLqefKjL_v0v74juLW1SU1AU,8058
|
|
26
|
-
fastmcp/server/server.py,sha256=ryN7o7G1gNFE1NsAuZVc3WpcmsBtcKOo-mXACN5NCoc,28814
|
|
27
|
-
fastmcp/tools/__init__.py,sha256=ocw-SFTtN6vQ8fgnlF8iNAOflRmh79xS1xdO0Bc3QPE,96
|
|
28
|
-
fastmcp/tools/tool.py,sha256=yPRqEM8sntDIbWPtZBy9evDg3BXupe0BKqnpvMwy7Sc,3872
|
|
29
|
-
fastmcp/tools/tool_manager.py,sha256=oUT8ExxIKKpJnFxCUDwThRxdK7WADD40e6yjGBYDPmI,3498
|
|
30
|
-
fastmcp/utilities/__init__.py,sha256=-imJ8S-rXmbXMWeDamldP-dHDqAPg_wwmPVz-LNX14E,31
|
|
31
|
-
fastmcp/utilities/decorators.py,sha256=AjhjsetQZF4YOPV5MTZmIxO21iFp_4fDIS3O2_KNCEg,2990
|
|
32
|
-
fastmcp/utilities/func_metadata.py,sha256=uh-u3gAjLD4kCcGf0ZkZZwBTTl-84JuANZTnDqP5ztI,7841
|
|
33
|
-
fastmcp/utilities/logging.py,sha256=1ipiOXzgWUp3Vih_JtEiLX7aAFmrUDZNr4KrZbofZTM,818
|
|
34
|
-
fastmcp/utilities/openapi.py,sha256=HiGCC5nh1sWtJRJ7DrFNhdjf1m-7SgyXWOE4nsRvyOc,48762
|
|
35
|
-
fastmcp/utilities/types.py,sha256=m2rPYMzO-ZFvvZ46N-1-Xqyw693K7yq9Z2xR4pVELyk,2091
|
|
36
|
-
fastmcp-2.1.1.dist-info/METADATA,sha256=Db4PK68mC7j-FCLfkUiyAg4hIaFgYho-X465u0c0sOs,26634
|
|
37
|
-
fastmcp-2.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
38
|
-
fastmcp-2.1.1.dist-info/entry_points.txt,sha256=ff8bMtKX1JvXyurMibAacMSKbJEPmac9ffAKU9mLnM8,44
|
|
39
|
-
fastmcp-2.1.1.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
|
|
40
|
-
fastmcp-2.1.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|