fastmcp 2.2.4__py3-none-any.whl → 2.2.5__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/prompts/prompt.py +43 -4
- fastmcp/prompts/prompt_manager.py +14 -3
- fastmcp/resources/resource.py +12 -2
- fastmcp/resources/resource_manager.py +20 -5
- fastmcp/resources/template.py +43 -4
- fastmcp/resources/types.py +55 -11
- fastmcp/server/openapi.py +19 -3
- fastmcp/server/proxy.py +37 -20
- fastmcp/server/server.py +31 -3
- fastmcp/tools/tool.py +22 -6
- {fastmcp-2.2.4.dist-info → fastmcp-2.2.5.dist-info}/METADATA +1 -1
- {fastmcp-2.2.4.dist-info → fastmcp-2.2.5.dist-info}/RECORD +15 -15
- {fastmcp-2.2.4.dist-info → fastmcp-2.2.5.dist-info}/WHEEL +0 -0
- {fastmcp-2.2.4.dist-info → fastmcp-2.2.5.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.2.4.dist-info → fastmcp-2.2.5.dist-info}/licenses/LICENSE +0 -0
fastmcp/prompts/prompt.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"""Base classes for FastMCP prompts."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations as _annotations
|
|
4
|
+
|
|
3
5
|
import inspect
|
|
4
6
|
import json
|
|
5
7
|
from collections.abc import Awaitable, Callable, Sequence
|
|
6
|
-
from typing import Annotated, Any, Literal
|
|
8
|
+
from typing import TYPE_CHECKING, Annotated, Any, Literal
|
|
7
9
|
|
|
8
10
|
import pydantic_core
|
|
9
11
|
from mcp.types import EmbeddedResource, ImageContent, TextContent
|
|
@@ -13,6 +15,12 @@ from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, validate_ca
|
|
|
13
15
|
|
|
14
16
|
from fastmcp.utilities.types import _convert_set_defaults
|
|
15
17
|
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from mcp.server.session import ServerSessionT
|
|
20
|
+
from mcp.shared.context import LifespanContextT
|
|
21
|
+
|
|
22
|
+
from fastmcp.server import Context
|
|
23
|
+
|
|
16
24
|
CONTENT_TYPES = TextContent | ImageContent | EmbeddedResource
|
|
17
25
|
|
|
18
26
|
|
|
@@ -72,6 +80,9 @@ class Prompt(BaseModel):
|
|
|
72
80
|
None, description="Arguments that can be passed to the prompt"
|
|
73
81
|
)
|
|
74
82
|
fn: Callable[..., PromptResult | Awaitable[PromptResult]]
|
|
83
|
+
context_kwarg: str | None = Field(
|
|
84
|
+
None, description="Name of the kwarg that should receive context"
|
|
85
|
+
)
|
|
75
86
|
|
|
76
87
|
@classmethod
|
|
77
88
|
def from_function(
|
|
@@ -80,7 +91,8 @@ class Prompt(BaseModel):
|
|
|
80
91
|
name: str | None = None,
|
|
81
92
|
description: str | None = None,
|
|
82
93
|
tags: set[str] | None = None,
|
|
83
|
-
|
|
94
|
+
context_kwarg: str | None = None,
|
|
95
|
+
) -> Prompt:
|
|
84
96
|
"""Create a Prompt from a function.
|
|
85
97
|
|
|
86
98
|
The function can return:
|
|
@@ -89,11 +101,24 @@ class Prompt(BaseModel):
|
|
|
89
101
|
- A dict (converted to a message)
|
|
90
102
|
- A sequence of any of the above
|
|
91
103
|
"""
|
|
104
|
+
from fastmcp import Context
|
|
105
|
+
|
|
92
106
|
func_name = name or fn.__name__
|
|
93
107
|
|
|
94
108
|
if func_name == "<lambda>":
|
|
95
109
|
raise ValueError("You must provide a name for lambda functions")
|
|
96
110
|
|
|
111
|
+
# Auto-detect context parameter if not provided
|
|
112
|
+
if context_kwarg is None:
|
|
113
|
+
if inspect.ismethod(fn) and hasattr(fn, "__func__"):
|
|
114
|
+
sig = inspect.signature(fn.__func__)
|
|
115
|
+
else:
|
|
116
|
+
sig = inspect.signature(fn)
|
|
117
|
+
for param_name, param in sig.parameters.items():
|
|
118
|
+
if param.annotation is Context:
|
|
119
|
+
context_kwarg = param_name
|
|
120
|
+
break
|
|
121
|
+
|
|
97
122
|
# Get schema from TypeAdapter - will fail if function isn't properly typed
|
|
98
123
|
parameters = TypeAdapter(fn).json_schema()
|
|
99
124
|
|
|
@@ -101,6 +126,10 @@ class Prompt(BaseModel):
|
|
|
101
126
|
arguments: list[PromptArgument] = []
|
|
102
127
|
if "properties" in parameters:
|
|
103
128
|
for param_name, param in parameters["properties"].items():
|
|
129
|
+
# Skip context parameter
|
|
130
|
+
if param_name == context_kwarg:
|
|
131
|
+
continue
|
|
132
|
+
|
|
104
133
|
required = param_name in parameters.get("required", [])
|
|
105
134
|
arguments.append(
|
|
106
135
|
PromptArgument(
|
|
@@ -119,9 +148,14 @@ class Prompt(BaseModel):
|
|
|
119
148
|
arguments=arguments,
|
|
120
149
|
fn=fn,
|
|
121
150
|
tags=tags or set(),
|
|
151
|
+
context_kwarg=context_kwarg,
|
|
122
152
|
)
|
|
123
153
|
|
|
124
|
-
async def render(
|
|
154
|
+
async def render(
|
|
155
|
+
self,
|
|
156
|
+
arguments: dict[str, Any] | None = None,
|
|
157
|
+
context: Context[ServerSessionT, LifespanContextT] | None = None,
|
|
158
|
+
) -> list[Message]:
|
|
125
159
|
"""Render the prompt with arguments."""
|
|
126
160
|
# Validate required arguments
|
|
127
161
|
if self.arguments:
|
|
@@ -132,8 +166,13 @@ class Prompt(BaseModel):
|
|
|
132
166
|
raise ValueError(f"Missing required arguments: {missing}")
|
|
133
167
|
|
|
134
168
|
try:
|
|
169
|
+
# Prepare arguments with context
|
|
170
|
+
kwargs = arguments.copy() if arguments else {}
|
|
171
|
+
if self.context_kwarg is not None and context is not None:
|
|
172
|
+
kwargs[self.context_kwarg] = context
|
|
173
|
+
|
|
135
174
|
# Call function and check if result is a coroutine
|
|
136
|
-
result = self.fn(**
|
|
175
|
+
result = self.fn(**kwargs)
|
|
137
176
|
if inspect.iscoroutine(result):
|
|
138
177
|
result = await result
|
|
139
178
|
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
"""Prompt management functionality."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations as _annotations
|
|
4
|
+
|
|
3
5
|
from collections.abc import Awaitable, Callable
|
|
4
|
-
from typing import Any
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
5
7
|
|
|
6
8
|
from fastmcp.exceptions import NotFoundError
|
|
7
9
|
from fastmcp.prompts.prompt import Message, Prompt, PromptResult
|
|
8
10
|
from fastmcp.settings import DuplicateBehavior
|
|
9
11
|
from fastmcp.utilities.logging import get_logger
|
|
10
12
|
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from mcp.server.session import ServerSessionT
|
|
15
|
+
from mcp.shared.context import LifespanContextT
|
|
16
|
+
|
|
17
|
+
from fastmcp.server import Context
|
|
18
|
+
|
|
11
19
|
logger = get_logger(__name__)
|
|
12
20
|
|
|
13
21
|
|
|
@@ -69,14 +77,17 @@ class PromptManager:
|
|
|
69
77
|
return prompt
|
|
70
78
|
|
|
71
79
|
async def render_prompt(
|
|
72
|
-
self,
|
|
80
|
+
self,
|
|
81
|
+
name: str,
|
|
82
|
+
arguments: dict[str, Any] | None = None,
|
|
83
|
+
context: Context[ServerSessionT, LifespanContextT] | None = None,
|
|
73
84
|
) -> list[Message]:
|
|
74
85
|
"""Render a prompt by name with arguments."""
|
|
75
86
|
prompt = self.get_prompt(name)
|
|
76
87
|
if not prompt:
|
|
77
88
|
raise NotFoundError(f"Unknown prompt: {name}")
|
|
78
89
|
|
|
79
|
-
return await prompt.render(arguments)
|
|
90
|
+
return await prompt.render(arguments, context=context)
|
|
80
91
|
|
|
81
92
|
def has_prompt(self, key: str) -> bool:
|
|
82
93
|
"""Check if a prompt exists."""
|
fastmcp/resources/resource.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"""Base classes and interfaces for FastMCP resources."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import abc
|
|
4
|
-
from typing import Annotated, Any
|
|
6
|
+
from typing import TYPE_CHECKING, Annotated, Any
|
|
5
7
|
|
|
6
8
|
from mcp.types import Resource as MCPResource
|
|
7
9
|
from pydantic import (
|
|
@@ -17,6 +19,12 @@ from pydantic import (
|
|
|
17
19
|
|
|
18
20
|
from fastmcp.utilities.types import _convert_set_defaults
|
|
19
21
|
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from mcp.server.session import ServerSessionT
|
|
24
|
+
from mcp.shared.context import LifespanContextT
|
|
25
|
+
|
|
26
|
+
from fastmcp.server import Context
|
|
27
|
+
|
|
20
28
|
|
|
21
29
|
class Resource(BaseModel, abc.ABC):
|
|
22
30
|
"""Base class for all resources."""
|
|
@@ -58,7 +66,9 @@ class Resource(BaseModel, abc.ABC):
|
|
|
58
66
|
raise ValueError("Either name or uri must be provided")
|
|
59
67
|
|
|
60
68
|
@abc.abstractmethod
|
|
61
|
-
async def read(
|
|
69
|
+
async def read(
|
|
70
|
+
self, context: Context[ServerSessionT, LifespanContextT] | None = None
|
|
71
|
+
) -> str | bytes:
|
|
62
72
|
"""Read the resource content."""
|
|
63
73
|
pass
|
|
64
74
|
|
|
@@ -61,9 +61,16 @@ class ResourceManager:
|
|
|
61
61
|
The added resource or template. If a resource or template with the same URI already exists,
|
|
62
62
|
returns the existing resource or template.
|
|
63
63
|
"""
|
|
64
|
+
from fastmcp.server.context import Context
|
|
65
|
+
|
|
64
66
|
# Check if this should be a template
|
|
65
67
|
has_uri_params = "{" in uri and "}" in uri
|
|
66
|
-
|
|
68
|
+
# check if the function has any parameters (other than injected context)
|
|
69
|
+
has_func_params = any(
|
|
70
|
+
p
|
|
71
|
+
for p in inspect.signature(fn).parameters.values()
|
|
72
|
+
if p.annotation is not Context
|
|
73
|
+
)
|
|
67
74
|
|
|
68
75
|
if has_uri_params or has_func_params:
|
|
69
76
|
return self.add_template_from_fn(
|
|
@@ -102,12 +109,12 @@ class ResourceManager:
|
|
|
102
109
|
The added resource. If a resource with the same URI already exists,
|
|
103
110
|
returns the existing resource.
|
|
104
111
|
"""
|
|
105
|
-
resource = FunctionResource(
|
|
112
|
+
resource = FunctionResource.from_function(
|
|
113
|
+
fn=fn,
|
|
106
114
|
uri=AnyUrl(uri),
|
|
107
115
|
name=name,
|
|
108
116
|
description=description,
|
|
109
117
|
mime_type=mime_type or "text/plain",
|
|
110
|
-
fn=fn,
|
|
111
118
|
tags=tags or set(),
|
|
112
119
|
)
|
|
113
120
|
return self.add_resource(resource)
|
|
@@ -212,9 +219,13 @@ class ResourceManager:
|
|
|
212
219
|
return True
|
|
213
220
|
return False
|
|
214
221
|
|
|
215
|
-
async def get_resource(self, uri: AnyUrl | str) -> Resource:
|
|
222
|
+
async def get_resource(self, uri: AnyUrl | str, context=None) -> Resource:
|
|
216
223
|
"""Get resource by URI, checking concrete resources first, then templates.
|
|
217
224
|
|
|
225
|
+
Args:
|
|
226
|
+
uri: The URI of the resource to get
|
|
227
|
+
context: Optional context object to pass to template resources
|
|
228
|
+
|
|
218
229
|
Raises:
|
|
219
230
|
NotFoundError: If no resource or template matching the URI is found.
|
|
220
231
|
"""
|
|
@@ -230,7 +241,11 @@ class ResourceManager:
|
|
|
230
241
|
# Try to match against the storage key (which might be a custom key)
|
|
231
242
|
if params := match_uri_template(uri_str, storage_key):
|
|
232
243
|
try:
|
|
233
|
-
return await template.create_resource(
|
|
244
|
+
return await template.create_resource(
|
|
245
|
+
uri_str,
|
|
246
|
+
params=params,
|
|
247
|
+
context=context,
|
|
248
|
+
)
|
|
234
249
|
except Exception as e:
|
|
235
250
|
raise ValueError(f"Error creating resource from template: {e}")
|
|
236
251
|
|
fastmcp/resources/template.py
CHANGED
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import inspect
|
|
6
6
|
import re
|
|
7
7
|
from collections.abc import Callable
|
|
8
|
-
from typing import Annotated, Any
|
|
8
|
+
from typing import TYPE_CHECKING, Annotated, Any
|
|
9
9
|
from urllib.parse import unquote
|
|
10
10
|
|
|
11
11
|
from mcp.types import ResourceTemplate as MCPResourceTemplate
|
|
@@ -22,6 +22,12 @@ from pydantic import (
|
|
|
22
22
|
from fastmcp.resources.types import FunctionResource, Resource
|
|
23
23
|
from fastmcp.utilities.types import _convert_set_defaults
|
|
24
24
|
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from mcp.server.session import ServerSessionT
|
|
27
|
+
from mcp.shared.context import LifespanContextT
|
|
28
|
+
|
|
29
|
+
from fastmcp.server import Context
|
|
30
|
+
|
|
25
31
|
|
|
26
32
|
def build_regex(template: str) -> re.Pattern:
|
|
27
33
|
parts = re.split(r"(\{[^}]+\})", template)
|
|
@@ -70,6 +76,9 @@ class ResourceTemplate(BaseModel):
|
|
|
70
76
|
parameters: dict[str, Any] = Field(
|
|
71
77
|
description="JSON schema for function parameters"
|
|
72
78
|
)
|
|
79
|
+
context_kwarg: str | None = Field(
|
|
80
|
+
None, description="Name of the kwarg that should receive context"
|
|
81
|
+
)
|
|
73
82
|
|
|
74
83
|
@field_validator("mime_type", mode="before")
|
|
75
84
|
@classmethod
|
|
@@ -88,18 +97,34 @@ class ResourceTemplate(BaseModel):
|
|
|
88
97
|
description: str | None = None,
|
|
89
98
|
mime_type: str | None = None,
|
|
90
99
|
tags: set[str] | None = None,
|
|
100
|
+
context_kwarg: str | None = None,
|
|
91
101
|
) -> ResourceTemplate:
|
|
92
102
|
"""Create a template from a function."""
|
|
103
|
+
from fastmcp import Context
|
|
104
|
+
|
|
93
105
|
func_name = name or fn.__name__
|
|
94
106
|
if func_name == "<lambda>":
|
|
95
107
|
raise ValueError("You must provide a name for lambda functions")
|
|
96
108
|
|
|
109
|
+
# Auto-detect context parameter if not provided
|
|
110
|
+
if context_kwarg is None:
|
|
111
|
+
if inspect.ismethod(fn) and hasattr(fn, "__func__"):
|
|
112
|
+
sig = inspect.signature(fn.__func__)
|
|
113
|
+
else:
|
|
114
|
+
sig = inspect.signature(fn)
|
|
115
|
+
for param_name, param in sig.parameters.items():
|
|
116
|
+
if param.annotation is Context:
|
|
117
|
+
context_kwarg = param_name
|
|
118
|
+
break
|
|
119
|
+
|
|
97
120
|
# Validate that URI params match function params
|
|
98
121
|
uri_params = set(re.findall(r"{(\w+)(?:\*)?}", uri_template))
|
|
99
122
|
if not uri_params:
|
|
100
123
|
raise ValueError("URI template must contain at least one parameter")
|
|
101
124
|
|
|
102
125
|
func_params = set(inspect.signature(fn).parameters.keys())
|
|
126
|
+
if context_kwarg:
|
|
127
|
+
func_params.discard(context_kwarg)
|
|
103
128
|
|
|
104
129
|
# get the parameters that are required
|
|
105
130
|
required_params = {
|
|
@@ -107,6 +132,8 @@ class ResourceTemplate(BaseModel):
|
|
|
107
132
|
for p in func_params
|
|
108
133
|
if inspect.signature(fn).parameters[p].default is inspect.Parameter.empty
|
|
109
134
|
}
|
|
135
|
+
if context_kwarg and context_kwarg in required_params:
|
|
136
|
+
required_params.discard(context_kwarg)
|
|
110
137
|
|
|
111
138
|
if not required_params.issubset(uri_params):
|
|
112
139
|
raise ValueError(
|
|
@@ -132,17 +159,28 @@ class ResourceTemplate(BaseModel):
|
|
|
132
159
|
fn=fn,
|
|
133
160
|
parameters=parameters,
|
|
134
161
|
tags=tags or set(),
|
|
162
|
+
context_kwarg=context_kwarg,
|
|
135
163
|
)
|
|
136
164
|
|
|
137
165
|
def matches(self, uri: str) -> dict[str, Any] | None:
|
|
138
166
|
"""Check if URI matches template and extract parameters."""
|
|
139
167
|
return match_uri_template(uri, self.uri_template)
|
|
140
168
|
|
|
141
|
-
async def create_resource(
|
|
169
|
+
async def create_resource(
|
|
170
|
+
self,
|
|
171
|
+
uri: str,
|
|
172
|
+
params: dict[str, Any],
|
|
173
|
+
context: Context[ServerSessionT, LifespanContextT] | None = None,
|
|
174
|
+
) -> Resource:
|
|
142
175
|
"""Create a resource from the template with the given parameters."""
|
|
143
176
|
try:
|
|
177
|
+
# Add context to parameters if needed
|
|
178
|
+
kwargs = params.copy()
|
|
179
|
+
if self.context_kwarg is not None and context is not None:
|
|
180
|
+
kwargs[self.context_kwarg] = context
|
|
181
|
+
|
|
144
182
|
# Call function and check if result is a coroutine
|
|
145
|
-
result = self.fn(**
|
|
183
|
+
result = self.fn(**kwargs)
|
|
146
184
|
if inspect.iscoroutine(result):
|
|
147
185
|
result = await result
|
|
148
186
|
|
|
@@ -151,8 +189,9 @@ class ResourceTemplate(BaseModel):
|
|
|
151
189
|
name=self.name,
|
|
152
190
|
description=self.description,
|
|
153
191
|
mime_type=self.mime_type,
|
|
154
|
-
fn=lambda: result, # Capture result in closure
|
|
192
|
+
fn=lambda **kwargs: result, # Capture result in closure
|
|
155
193
|
tags=self.tags,
|
|
194
|
+
context_kwarg=self.context_kwarg,
|
|
156
195
|
)
|
|
157
196
|
except Exception as e:
|
|
158
197
|
raise ValueError(f"Error creating resource from template: {e}")
|
fastmcp/resources/types.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"""Concrete resource implementations."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import inspect
|
|
4
6
|
import json
|
|
5
7
|
from collections.abc import Callable
|
|
6
8
|
from pathlib import Path
|
|
7
|
-
from typing import Any
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
8
10
|
|
|
9
11
|
import anyio
|
|
10
12
|
import anyio.to_thread
|
|
@@ -13,15 +15,24 @@ import pydantic.json
|
|
|
13
15
|
import pydantic_core
|
|
14
16
|
from pydantic import Field, ValidationInfo
|
|
15
17
|
|
|
18
|
+
import fastmcp
|
|
16
19
|
from fastmcp.resources.resource import Resource
|
|
17
20
|
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from mcp.server.session import ServerSessionT
|
|
23
|
+
from mcp.shared.context import LifespanContextT
|
|
24
|
+
|
|
25
|
+
from fastmcp.server import Context
|
|
26
|
+
|
|
18
27
|
|
|
19
28
|
class TextResource(Resource):
|
|
20
29
|
"""A resource that reads from a string."""
|
|
21
30
|
|
|
22
31
|
text: str = Field(description="Text content of the resource")
|
|
23
32
|
|
|
24
|
-
async def read(
|
|
33
|
+
async def read(
|
|
34
|
+
self, context: Context[ServerSessionT, LifespanContextT] | None = None
|
|
35
|
+
) -> str:
|
|
25
36
|
"""Read the text content."""
|
|
26
37
|
return self.text
|
|
27
38
|
|
|
@@ -31,7 +42,9 @@ class BinaryResource(Resource):
|
|
|
31
42
|
|
|
32
43
|
data: bytes = Field(description="Binary content of the resource")
|
|
33
44
|
|
|
34
|
-
async def read(
|
|
45
|
+
async def read(
|
|
46
|
+
self, context: Context[ServerSessionT, LifespanContextT] | None = None
|
|
47
|
+
) -> bytes:
|
|
35
48
|
"""Read the binary content."""
|
|
36
49
|
return self.data
|
|
37
50
|
|
|
@@ -50,15 +63,40 @@ class FunctionResource(Resource):
|
|
|
50
63
|
"""
|
|
51
64
|
|
|
52
65
|
fn: Callable[[], Any]
|
|
66
|
+
context_kwarg: str | None = Field(
|
|
67
|
+
default=None, description="Name of the kwarg that should receive context"
|
|
68
|
+
)
|
|
53
69
|
|
|
54
|
-
|
|
70
|
+
@classmethod
|
|
71
|
+
def from_function(
|
|
72
|
+
cls, fn: Callable[[], Any], context_kwarg: str | None = None, **kwargs
|
|
73
|
+
) -> FunctionResource:
|
|
74
|
+
if context_kwarg is None:
|
|
75
|
+
parameters = inspect.signature(fn).parameters
|
|
76
|
+
context_param = next(
|
|
77
|
+
(p for p in parameters.values() if p.annotation is fastmcp.Context),
|
|
78
|
+
None,
|
|
79
|
+
)
|
|
80
|
+
if context_param is not None:
|
|
81
|
+
context_kwarg = context_param.name
|
|
82
|
+
return cls(fn=fn, context_kwarg=context_kwarg, **kwargs)
|
|
83
|
+
|
|
84
|
+
async def read(
|
|
85
|
+
self,
|
|
86
|
+
context: Context[ServerSessionT, LifespanContextT] | None = None,
|
|
87
|
+
) -> str | bytes:
|
|
55
88
|
"""Read the resource by calling the wrapped function."""
|
|
56
89
|
try:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
90
|
+
kwargs = {}
|
|
91
|
+
if self.context_kwarg is not None:
|
|
92
|
+
kwargs[self.context_kwarg] = context
|
|
93
|
+
|
|
94
|
+
result = self.fn(**kwargs)
|
|
95
|
+
if inspect.iscoroutinefunction(self.fn):
|
|
96
|
+
result = await result
|
|
97
|
+
|
|
60
98
|
if isinstance(result, Resource):
|
|
61
|
-
return await result.read()
|
|
99
|
+
return await result.read(context=context)
|
|
62
100
|
if isinstance(result, bytes):
|
|
63
101
|
return result
|
|
64
102
|
if isinstance(result, str):
|
|
@@ -105,7 +143,9 @@ class FileResource(Resource):
|
|
|
105
143
|
mime_type = info.data.get("mime_type", "text/plain")
|
|
106
144
|
return not mime_type.startswith("text/")
|
|
107
145
|
|
|
108
|
-
async def read(
|
|
146
|
+
async def read(
|
|
147
|
+
self, context: Context[ServerSessionT, LifespanContextT] | None = None
|
|
148
|
+
) -> str | bytes:
|
|
109
149
|
"""Read the file content."""
|
|
110
150
|
try:
|
|
111
151
|
if self.is_binary:
|
|
@@ -123,7 +163,9 @@ class HttpResource(Resource):
|
|
|
123
163
|
default="application/json", description="MIME type of the resource content"
|
|
124
164
|
)
|
|
125
165
|
|
|
126
|
-
async def read(
|
|
166
|
+
async def read(
|
|
167
|
+
self, context: Context[ServerSessionT, LifespanContextT] | None = None
|
|
168
|
+
) -> str | bytes:
|
|
127
169
|
"""Read the HTTP content."""
|
|
128
170
|
async with httpx.AsyncClient() as client:
|
|
129
171
|
response = await client.get(self.url)
|
|
@@ -175,7 +217,9 @@ class DirectoryResource(Resource):
|
|
|
175
217
|
except Exception as e:
|
|
176
218
|
raise ValueError(f"Error listing directory {self.path}: {e}")
|
|
177
219
|
|
|
178
|
-
async def read(
|
|
220
|
+
async def read(
|
|
221
|
+
self, context: Context[ServerSessionT, LifespanContextT] | None = None
|
|
222
|
+
) -> str: # Always returns JSON string
|
|
179
223
|
"""Read the directory listing."""
|
|
180
224
|
try:
|
|
181
225
|
files = await anyio.to_thread.run_sync(self.list_files)
|
fastmcp/server/openapi.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"""FastMCP server implementation for OpenAPI integration."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import enum
|
|
4
6
|
import json
|
|
5
7
|
import re
|
|
6
8
|
from dataclasses import dataclass
|
|
7
9
|
from re import Pattern
|
|
8
|
-
from typing import Any, Literal
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
9
11
|
|
|
10
12
|
import httpx
|
|
11
13
|
from mcp.types import TextContent
|
|
@@ -22,6 +24,12 @@ from fastmcp.utilities.openapi import (
|
|
|
22
24
|
format_description_with_responses,
|
|
23
25
|
)
|
|
24
26
|
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from mcp.server.session import ServerSessionT
|
|
29
|
+
from mcp.shared.context import LifespanContextT
|
|
30
|
+
|
|
31
|
+
from fastmcp.server import Context
|
|
32
|
+
|
|
25
33
|
logger = get_logger(__name__)
|
|
26
34
|
|
|
27
35
|
HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]
|
|
@@ -257,7 +265,9 @@ class OpenAPIResource(Resource):
|
|
|
257
265
|
self._client = client
|
|
258
266
|
self._route = route
|
|
259
267
|
|
|
260
|
-
async def read(
|
|
268
|
+
async def read(
|
|
269
|
+
self, context: Context[ServerSessionT, LifespanContextT] | None = None
|
|
270
|
+
) -> str | bytes:
|
|
261
271
|
"""Fetch the resource data by making an HTTP request."""
|
|
262
272
|
try:
|
|
263
273
|
# Extract path parameters from the URI if present
|
|
@@ -347,11 +357,17 @@ class OpenAPIResourceTemplate(ResourceTemplate):
|
|
|
347
357
|
fn=lambda **kwargs: None,
|
|
348
358
|
parameters=parameters,
|
|
349
359
|
tags=tags,
|
|
360
|
+
context_kwarg=None,
|
|
350
361
|
)
|
|
351
362
|
self._client = client
|
|
352
363
|
self._route = route
|
|
353
364
|
|
|
354
|
-
async def create_resource(
|
|
365
|
+
async def create_resource(
|
|
366
|
+
self,
|
|
367
|
+
uri: str,
|
|
368
|
+
params: dict[str, Any],
|
|
369
|
+
context: Context[ServerSessionT, LifespanContextT] | None = None,
|
|
370
|
+
) -> Resource:
|
|
355
371
|
"""Create a resource with the given parameters."""
|
|
356
372
|
# Generate a URI for this resource instance
|
|
357
373
|
uri_parts = []
|
fastmcp/server/proxy.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
2
4
|
from urllib.parse import quote
|
|
3
5
|
|
|
4
6
|
import mcp.types
|
|
@@ -25,6 +27,12 @@ from fastmcp.tools.tool import Tool
|
|
|
25
27
|
from fastmcp.utilities.func_metadata import func_metadata
|
|
26
28
|
from fastmcp.utilities.logging import get_logger
|
|
27
29
|
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from mcp.server.session import ServerSessionT
|
|
32
|
+
from mcp.shared.context import LifespanContextT
|
|
33
|
+
|
|
34
|
+
from fastmcp.server import Context
|
|
35
|
+
|
|
28
36
|
logger = get_logger(__name__)
|
|
29
37
|
|
|
30
38
|
|
|
@@ -33,12 +41,12 @@ def _proxy_passthrough():
|
|
|
33
41
|
|
|
34
42
|
|
|
35
43
|
class ProxyTool(Tool):
|
|
36
|
-
def __init__(self, client:
|
|
44
|
+
def __init__(self, client: Client, **kwargs):
|
|
37
45
|
super().__init__(**kwargs)
|
|
38
46
|
self._client = client
|
|
39
47
|
|
|
40
48
|
@classmethod
|
|
41
|
-
async def from_client(cls, client:
|
|
49
|
+
async def from_client(cls, client: Client, tool: mcp.types.Tool) -> ProxyTool:
|
|
42
50
|
return cls(
|
|
43
51
|
client=client,
|
|
44
52
|
name=tool.name,
|
|
@@ -50,7 +58,9 @@ class ProxyTool(Tool):
|
|
|
50
58
|
)
|
|
51
59
|
|
|
52
60
|
async def run(
|
|
53
|
-
self,
|
|
61
|
+
self,
|
|
62
|
+
arguments: dict[str, Any],
|
|
63
|
+
context: Context[ServerSessionT, LifespanContextT] | None = None,
|
|
54
64
|
) -> Any:
|
|
55
65
|
# the client context manager will swallow any exceptions inside a TaskGroup
|
|
56
66
|
# so we return the raw result and raise an exception ourselves
|
|
@@ -64,17 +74,15 @@ class ProxyTool(Tool):
|
|
|
64
74
|
|
|
65
75
|
|
|
66
76
|
class ProxyResource(Resource):
|
|
67
|
-
def __init__(
|
|
68
|
-
self, client: "Client", *, _value: str | bytes | None = None, **kwargs
|
|
69
|
-
):
|
|
77
|
+
def __init__(self, client: Client, *, _value: str | bytes | None = None, **kwargs):
|
|
70
78
|
super().__init__(**kwargs)
|
|
71
79
|
self._client = client
|
|
72
80
|
self._value = _value
|
|
73
81
|
|
|
74
82
|
@classmethod
|
|
75
83
|
async def from_client(
|
|
76
|
-
cls, client:
|
|
77
|
-
) ->
|
|
84
|
+
cls, client: Client, resource: mcp.types.Resource
|
|
85
|
+
) -> ProxyResource:
|
|
78
86
|
return cls(
|
|
79
87
|
client=client,
|
|
80
88
|
uri=resource.uri,
|
|
@@ -83,7 +91,9 @@ class ProxyResource(Resource):
|
|
|
83
91
|
mime_type=resource.mimeType,
|
|
84
92
|
)
|
|
85
93
|
|
|
86
|
-
async def read(
|
|
94
|
+
async def read(
|
|
95
|
+
self, context: Context[ServerSessionT, LifespanContextT] | None = None
|
|
96
|
+
) -> str | bytes:
|
|
87
97
|
if self._value is not None:
|
|
88
98
|
return self._value
|
|
89
99
|
|
|
@@ -98,14 +108,14 @@ class ProxyResource(Resource):
|
|
|
98
108
|
|
|
99
109
|
|
|
100
110
|
class ProxyTemplate(ResourceTemplate):
|
|
101
|
-
def __init__(self, client:
|
|
111
|
+
def __init__(self, client: Client, **kwargs):
|
|
102
112
|
super().__init__(**kwargs)
|
|
103
113
|
self._client = client
|
|
104
114
|
|
|
105
115
|
@classmethod
|
|
106
116
|
async def from_client(
|
|
107
|
-
cls, client:
|
|
108
|
-
) ->
|
|
117
|
+
cls, client: Client, template: mcp.types.ResourceTemplate
|
|
118
|
+
) -> ProxyTemplate:
|
|
109
119
|
return cls(
|
|
110
120
|
client=client,
|
|
111
121
|
uri_template=template.uriTemplate,
|
|
@@ -115,7 +125,12 @@ class ProxyTemplate(ResourceTemplate):
|
|
|
115
125
|
parameters={},
|
|
116
126
|
)
|
|
117
127
|
|
|
118
|
-
async def create_resource(
|
|
128
|
+
async def create_resource(
|
|
129
|
+
self,
|
|
130
|
+
uri: str,
|
|
131
|
+
params: dict[str, Any],
|
|
132
|
+
context: Context[ServerSessionT, LifespanContextT] | None = None,
|
|
133
|
+
) -> ProxyResource:
|
|
119
134
|
# dont use the provided uri, because it may not be the same as the
|
|
120
135
|
# uri_template on the remote server.
|
|
121
136
|
# quote params to ensure they are valid for the uri_template
|
|
@@ -144,14 +159,12 @@ class ProxyTemplate(ResourceTemplate):
|
|
|
144
159
|
|
|
145
160
|
|
|
146
161
|
class ProxyPrompt(Prompt):
|
|
147
|
-
def __init__(self, client:
|
|
162
|
+
def __init__(self, client: Client, **kwargs):
|
|
148
163
|
super().__init__(**kwargs)
|
|
149
164
|
self._client = client
|
|
150
165
|
|
|
151
166
|
@classmethod
|
|
152
|
-
async def from_client(
|
|
153
|
-
cls, client: "Client", prompt: mcp.types.Prompt
|
|
154
|
-
) -> "ProxyPrompt":
|
|
167
|
+
async def from_client(cls, client: Client, prompt: mcp.types.Prompt) -> ProxyPrompt:
|
|
155
168
|
return cls(
|
|
156
169
|
client=client,
|
|
157
170
|
name=prompt.name,
|
|
@@ -160,14 +173,18 @@ class ProxyPrompt(Prompt):
|
|
|
160
173
|
fn=_proxy_passthrough,
|
|
161
174
|
)
|
|
162
175
|
|
|
163
|
-
async def render(
|
|
176
|
+
async def render(
|
|
177
|
+
self,
|
|
178
|
+
arguments: dict[str, Any],
|
|
179
|
+
context: Context[ServerSessionT, LifespanContextT] | None = None,
|
|
180
|
+
) -> list[Message]:
|
|
164
181
|
async with self._client:
|
|
165
182
|
result = await self._client.get_prompt(self.name, arguments)
|
|
166
183
|
return [Message(role=m.role, content=m.content) for m in result]
|
|
167
184
|
|
|
168
185
|
|
|
169
186
|
class FastMCPProxy(FastMCP):
|
|
170
|
-
def __init__(self, client:
|
|
187
|
+
def __init__(self, client: Client, **kwargs):
|
|
171
188
|
super().__init__(**kwargs)
|
|
172
189
|
self.client = client
|
|
173
190
|
|
fastmcp/server/server.py
CHANGED
|
@@ -398,9 +398,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
398
398
|
server.
|
|
399
399
|
"""
|
|
400
400
|
if self._resource_manager.has_resource(uri):
|
|
401
|
-
|
|
401
|
+
context = self.get_context()
|
|
402
|
+
resource = await self._resource_manager.get_resource(uri, context=context)
|
|
402
403
|
try:
|
|
403
|
-
content = await resource.read()
|
|
404
|
+
content = await resource.read(context=context)
|
|
404
405
|
return [
|
|
405
406
|
ReadResourceContents(content=content, mime_type=resource.mime_type)
|
|
406
407
|
]
|
|
@@ -424,7 +425,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
424
425
|
|
|
425
426
|
"""
|
|
426
427
|
if self._prompt_manager.has_prompt(name):
|
|
427
|
-
|
|
428
|
+
context = self.get_context()
|
|
429
|
+
messages = await self._prompt_manager.render_prompt(
|
|
430
|
+
name, arguments=arguments or {}, context=context
|
|
431
|
+
)
|
|
428
432
|
return GetPromptResult(messages=pydantic_core.to_jsonable_python(messages))
|
|
429
433
|
else:
|
|
430
434
|
for server in self._mounted_servers.values():
|
|
@@ -562,6 +566,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
562
566
|
- bytes for binary content
|
|
563
567
|
- other types will be converted to JSON
|
|
564
568
|
|
|
569
|
+
Resources can optionally request a Context object by adding a parameter with the
|
|
570
|
+
Context type annotation. The context provides access to MCP capabilities like
|
|
571
|
+
logging, progress reporting, and session information.
|
|
572
|
+
|
|
565
573
|
If the URI contains parameters (e.g. "resource://{param}") or the function
|
|
566
574
|
has parameters, it will be registered as a template resource.
|
|
567
575
|
|
|
@@ -586,6 +594,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
586
594
|
def get_weather(city: str) -> str:
|
|
587
595
|
return f"Weather for {city}"
|
|
588
596
|
|
|
597
|
+
@server.resource("resource://{city}/weather")
|
|
598
|
+
def get_weather_with_context(city: str, ctx: Context) -> str:
|
|
599
|
+
ctx.info(f"Fetching weather for {city}")
|
|
600
|
+
return f"Weather for {city}"
|
|
601
|
+
|
|
589
602
|
@server.resource("resource://{city}/weather")
|
|
590
603
|
async def get_weather(city: str) -> str:
|
|
591
604
|
data = await fetch_weather(city)
|
|
@@ -639,6 +652,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
639
652
|
) -> Callable[[AnyFunction], AnyFunction]:
|
|
640
653
|
"""Decorator to register a prompt.
|
|
641
654
|
|
|
655
|
+
Prompts can optionally request a Context object by adding a parameter with the
|
|
656
|
+
Context type annotation. The context provides access to MCP capabilities like
|
|
657
|
+
logging, progress reporting, and session information.
|
|
658
|
+
|
|
642
659
|
Args:
|
|
643
660
|
name: Optional name for the prompt (defaults to function name)
|
|
644
661
|
description: Optional description of what the prompt does
|
|
@@ -655,6 +672,17 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
655
672
|
}
|
|
656
673
|
]
|
|
657
674
|
|
|
675
|
+
@server.prompt()
|
|
676
|
+
def analyze_with_context(table_name: str, ctx: Context) -> list[Message]:
|
|
677
|
+
ctx.info(f"Analyzing table {table_name}")
|
|
678
|
+
schema = read_table_schema(table_name)
|
|
679
|
+
return [
|
|
680
|
+
{
|
|
681
|
+
"role": "user",
|
|
682
|
+
"content": f"Analyze this schema:\n{schema}"
|
|
683
|
+
}
|
|
684
|
+
]
|
|
685
|
+
|
|
658
686
|
@server.prompt()
|
|
659
687
|
async def analyze_file(path: str) -> list[Message]:
|
|
660
688
|
content = await read_file(path)
|
fastmcp/tools/tool.py
CHANGED
|
@@ -101,13 +101,16 @@ class Tool(BaseModel):
|
|
|
101
101
|
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
102
102
|
"""Run the tool with arguments."""
|
|
103
103
|
try:
|
|
104
|
-
|
|
105
|
-
self.fn,
|
|
106
|
-
self.is_async,
|
|
107
|
-
arguments,
|
|
104
|
+
pass_args = (
|
|
108
105
|
{self.context_kwarg: context}
|
|
109
106
|
if self.context_kwarg is not None
|
|
110
|
-
else None
|
|
107
|
+
else None
|
|
108
|
+
)
|
|
109
|
+
result = await self.fn_metadata.call_fn_with_arg_validation(
|
|
110
|
+
fn=self.fn,
|
|
111
|
+
fn_is_async=self.is_async,
|
|
112
|
+
arguments_to_validate=arguments,
|
|
113
|
+
arguments_to_pass_directly=pass_args,
|
|
111
114
|
)
|
|
112
115
|
return _convert_to_content(result)
|
|
113
116
|
except Exception as e:
|
|
@@ -163,9 +166,22 @@ def _convert_to_content(
|
|
|
163
166
|
|
|
164
167
|
return other_content + mcp_types
|
|
165
168
|
|
|
169
|
+
# if the result is a bytes object, convert it to a text content object
|
|
166
170
|
if not isinstance(result, str):
|
|
167
171
|
try:
|
|
168
|
-
|
|
172
|
+
jsonable_result = pydantic_core.to_jsonable_python(result)
|
|
173
|
+
if jsonable_result is None:
|
|
174
|
+
return [TextContent(type="text", text="null")]
|
|
175
|
+
elif isinstance(jsonable_result, bool):
|
|
176
|
+
return [
|
|
177
|
+
TextContent(
|
|
178
|
+
type="text", text="true" if jsonable_result else "false"
|
|
179
|
+
)
|
|
180
|
+
]
|
|
181
|
+
elif isinstance(jsonable_result, str | int | float):
|
|
182
|
+
return [TextContent(type="text", text=str(jsonable_result))]
|
|
183
|
+
else:
|
|
184
|
+
return [TextContent(type="text", text=json.dumps(jsonable_result))]
|
|
169
185
|
except Exception:
|
|
170
186
|
result = str(result)
|
|
171
187
|
|
|
@@ -21,20 +21,20 @@ fastmcp/contrib/mcp_mixin/__init__.py,sha256=aw9IQ1ssNjCgws4ZNt8bkdpossAAGVAwwjB
|
|
|
21
21
|
fastmcp/contrib/mcp_mixin/example.py,sha256=GnunkXmtG5hLLTUsM8aW5ZURU52Z8vI4tNLl-fK7Dg0,1228
|
|
22
22
|
fastmcp/contrib/mcp_mixin/mcp_mixin.py,sha256=cfIRbnSxsVzglTD-auyTE0izVQeHP7Oz18qzYoBZJgg,7899
|
|
23
23
|
fastmcp/prompts/__init__.py,sha256=LtPAv2JKIu54AwUd3iwv-HUd4DPcwgEqy6itEd3BH_E,194
|
|
24
|
-
fastmcp/prompts/prompt.py,sha256=
|
|
25
|
-
fastmcp/prompts/prompt_manager.py,sha256=
|
|
24
|
+
fastmcp/prompts/prompt.py,sha256=xNSlvs-vRB-kz7xnMYf2RwkiilbE0HcOoXgMS6gUogk,7974
|
|
25
|
+
fastmcp/prompts/prompt_manager.py,sha256=4CInlSWASjHUfGu2i0ig2ZICzHHxCMFGTXhuXgYdukQ,3237
|
|
26
26
|
fastmcp/resources/__init__.py,sha256=t0x1j8lc74rjUKtXe9H5Gs4fpQt82K4NgBK6Y7A0xTg,467
|
|
27
|
-
fastmcp/resources/resource.py,sha256=
|
|
28
|
-
fastmcp/resources/resource_manager.py,sha256=
|
|
29
|
-
fastmcp/resources/template.py,sha256=
|
|
30
|
-
fastmcp/resources/types.py,sha256=
|
|
27
|
+
fastmcp/resources/resource.py,sha256=GGG0XugoIMbgAJRMVxsBcZLbt19W3COy8PyTh_uoWjs,2705
|
|
28
|
+
fastmcp/resources/resource_manager.py,sha256=yfQNCEUooZiQ8LNslnAzjQp4Vh-y1YOlFyGEQZ0BtAg,9586
|
|
29
|
+
fastmcp/resources/template.py,sha256=oa85KiuTjh3C7aZvMwmO4fwbTi6IvwlQ3fxizJuv3dk,7261
|
|
30
|
+
fastmcp/resources/types.py,sha256=c1z6BQSosgrlPQ3v67DuXCvDjCJMq9Xl45npEpyk0ik,7710
|
|
31
31
|
fastmcp/server/__init__.py,sha256=pdkghG11VLMZiluQ-4_rl2JK1LMWmV003m9dDRUN8W4,92
|
|
32
32
|
fastmcp/server/context.py,sha256=s1885AZRipKB3VltfaO3VEtMxGefKs8fdZByj-4tbNI,7120
|
|
33
|
-
fastmcp/server/openapi.py,sha256=
|
|
34
|
-
fastmcp/server/proxy.py,sha256=
|
|
35
|
-
fastmcp/server/server.py,sha256=
|
|
33
|
+
fastmcp/server/openapi.py,sha256=hFMOVe-bzudxP8SE-CqQhUWlUCVF5inGfMVL28HlqDs,21179
|
|
34
|
+
fastmcp/server/proxy.py,sha256=xOufto2gIfLk2BZfjhpLdZOlKDlJk5Rn6hCP0pzvaCU,10110
|
|
35
|
+
fastmcp/server/server.py,sha256=89RreIOw0siZmc6SlVlYWm6d6cFvjfVPY7mczXCNGFM,33032
|
|
36
36
|
fastmcp/tools/__init__.py,sha256=ocw-SFTtN6vQ8fgnlF8iNAOflRmh79xS1xdO0Bc3QPE,96
|
|
37
|
-
fastmcp/tools/tool.py,sha256=
|
|
37
|
+
fastmcp/tools/tool.py,sha256=k797XAeXdcUuBfeuvxkEy8xckXi7xSzQVgkzL876rBQ,6755
|
|
38
38
|
fastmcp/tools/tool_manager.py,sha256=hClv7fwj0cQSSwW0i-Swt7xiVqR4T9LVmr1Tp704nW4,3283
|
|
39
39
|
fastmcp/utilities/__init__.py,sha256=-imJ8S-rXmbXMWeDamldP-dHDqAPg_wwmPVz-LNX14E,31
|
|
40
40
|
fastmcp/utilities/decorators.py,sha256=AjhjsetQZF4YOPV5MTZmIxO21iFp_4fDIS3O2_KNCEg,2990
|
|
@@ -42,8 +42,8 @@ fastmcp/utilities/func_metadata.py,sha256=iYXnx7MILOSL8mUQ6Rtq_6U7qA08OkoEN2APY8
|
|
|
42
42
|
fastmcp/utilities/logging.py,sha256=zav8pnFxG_fvGJHUV2XpobmT9WVrmv1mlQBSCz-CPx4,1159
|
|
43
43
|
fastmcp/utilities/openapi.py,sha256=PrH3usbTblaVC6jIH1UGiPEfgB2sSCLj33zA5dH7o_s,45193
|
|
44
44
|
fastmcp/utilities/types.py,sha256=m2rPYMzO-ZFvvZ46N-1-Xqyw693K7yq9Z2xR4pVELyk,2091
|
|
45
|
-
fastmcp-2.2.
|
|
46
|
-
fastmcp-2.2.
|
|
47
|
-
fastmcp-2.2.
|
|
48
|
-
fastmcp-2.2.
|
|
49
|
-
fastmcp-2.2.
|
|
45
|
+
fastmcp-2.2.5.dist-info/METADATA,sha256=JKwmnn7QiN_KbNkC3ZyTpTEoUOTIDHPoCZFraw3eMR8,27769
|
|
46
|
+
fastmcp-2.2.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
47
|
+
fastmcp-2.2.5.dist-info/entry_points.txt,sha256=ff8bMtKX1JvXyurMibAacMSKbJEPmac9ffAKU9mLnM8,44
|
|
48
|
+
fastmcp-2.2.5.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
|
|
49
|
+
fastmcp-2.2.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|