fastmcp 2.2.4__py3-none-any.whl → 2.2.6__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/__init__.py +1 -0
- fastmcp/client/base.py +0 -1
- fastmcp/client/client.py +12 -8
- fastmcp/client/logging.py +13 -0
- fastmcp/client/sampling.py +2 -0
- fastmcp/client/transports.py +37 -4
- 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/context.py +15 -13
- fastmcp/server/openapi.py +86 -31
- fastmcp/server/proxy.py +38 -21
- fastmcp/server/server.py +49 -15
- fastmcp/tools/tool.py +22 -6
- {fastmcp-2.2.4.dist-info → fastmcp-2.2.6.dist-info}/METADATA +2 -2
- {fastmcp-2.2.4.dist-info → fastmcp-2.2.6.dist-info}/RECORD +22 -21
- {fastmcp-2.2.4.dist-info → fastmcp-2.2.6.dist-info}/WHEEL +0 -0
- {fastmcp-2.2.4.dist-info → fastmcp-2.2.6.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.2.4.dist-info → fastmcp-2.2.6.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/context.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations as _annotations
|
|
2
2
|
|
|
3
|
-
from typing import Any, Generic
|
|
3
|
+
from typing import Any, Generic
|
|
4
4
|
|
|
5
|
+
from mcp import LoggingLevel
|
|
5
6
|
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
|
6
7
|
from mcp.server.session import ServerSessionT
|
|
7
8
|
from mcp.shared.context import LifespanContextT, RequestContext
|
|
@@ -122,19 +123,20 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]):
|
|
|
122
123
|
|
|
123
124
|
async def log(
|
|
124
125
|
self,
|
|
125
|
-
level: Literal["debug", "info", "warning", "error"],
|
|
126
126
|
message: str,
|
|
127
|
-
|
|
127
|
+
level: LoggingLevel | None = None,
|
|
128
128
|
logger_name: str | None = None,
|
|
129
129
|
) -> None:
|
|
130
130
|
"""Send a log message to the client.
|
|
131
131
|
|
|
132
132
|
Args:
|
|
133
|
-
level: Log level (debug, info, warning, error)
|
|
134
133
|
message: Log message
|
|
134
|
+
level: Optional log level. One of "debug", "info", "notice", "warning", "error", "critical",
|
|
135
|
+
"alert", or "emergency". Default is "info".
|
|
135
136
|
logger_name: Optional logger name
|
|
136
|
-
**extra: Additional structured data to include
|
|
137
137
|
"""
|
|
138
|
+
if level is None:
|
|
139
|
+
level = "info"
|
|
138
140
|
await self.request_context.session.send_log_message(
|
|
139
141
|
level=level, data=message, logger=logger_name
|
|
140
142
|
)
|
|
@@ -159,21 +161,21 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]):
|
|
|
159
161
|
return self.request_context.session
|
|
160
162
|
|
|
161
163
|
# Convenience methods for common log levels
|
|
162
|
-
async def debug(self, message: str,
|
|
164
|
+
async def debug(self, message: str, logger_name: str | None = None) -> None:
|
|
163
165
|
"""Send a debug log message."""
|
|
164
|
-
await self.log("debug", message,
|
|
166
|
+
await self.log(level="debug", message=message, logger_name=logger_name)
|
|
165
167
|
|
|
166
|
-
async def info(self, message: str,
|
|
168
|
+
async def info(self, message: str, logger_name: str | None = None) -> None:
|
|
167
169
|
"""Send an info log message."""
|
|
168
|
-
await self.log("info", message,
|
|
170
|
+
await self.log(level="info", message=message, logger_name=logger_name)
|
|
169
171
|
|
|
170
|
-
async def warning(self, message: str,
|
|
172
|
+
async def warning(self, message: str, logger_name: str | None = None) -> None:
|
|
171
173
|
"""Send a warning log message."""
|
|
172
|
-
await self.log("warning", message,
|
|
174
|
+
await self.log(level="warning", message=message, logger_name=logger_name)
|
|
173
175
|
|
|
174
|
-
async def error(self, message: str,
|
|
176
|
+
async def error(self, message: str, logger_name: str | None = None) -> None:
|
|
175
177
|
"""Send an error log message."""
|
|
176
|
-
await self.log("error", message,
|
|
178
|
+
await self.log(level="error", message=message, logger_name=logger_name)
|
|
177
179
|
|
|
178
180
|
async def list_roots(self) -> list[Root]:
|
|
179
181
|
"""List the roots available to the server, as indicated by the client."""
|
fastmcp/server/openapi.py
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
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
|
-
from mcp.types import TextContent
|
|
13
|
+
from mcp.types import EmbeddedResource, ImageContent, TextContent
|
|
12
14
|
from pydantic.networks import AnyUrl
|
|
13
15
|
|
|
14
16
|
from fastmcp.resources import Resource, ResourceTemplate
|
|
15
17
|
from fastmcp.server.server import FastMCP
|
|
16
|
-
from fastmcp.tools.tool import Tool
|
|
18
|
+
from fastmcp.tools.tool import Tool, _convert_to_content
|
|
17
19
|
from fastmcp.utilities import openapi
|
|
18
20
|
from fastmcp.utilities.func_metadata import func_metadata
|
|
19
21
|
from fastmcp.utilities.logging import get_logger
|
|
@@ -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"]
|
|
@@ -117,6 +125,7 @@ class OpenAPITool(Tool):
|
|
|
117
125
|
fn_metadata: Any,
|
|
118
126
|
is_async: bool = True,
|
|
119
127
|
tags: set[str] = set(),
|
|
128
|
+
timeout: float | None = None,
|
|
120
129
|
):
|
|
121
130
|
super().__init__(
|
|
122
131
|
name=name,
|
|
@@ -130,6 +139,7 @@ class OpenAPITool(Tool):
|
|
|
130
139
|
)
|
|
131
140
|
self._client = client
|
|
132
141
|
self._route = route
|
|
142
|
+
self._timeout = timeout
|
|
133
143
|
|
|
134
144
|
async def _execute_request(self, *args, **kwargs):
|
|
135
145
|
"""Execute the HTTP request based on the route configuration."""
|
|
@@ -139,19 +149,37 @@ class OpenAPITool(Tool):
|
|
|
139
149
|
path = self._route.path
|
|
140
150
|
|
|
141
151
|
# Replace path parameters with values from kwargs
|
|
152
|
+
# Path parameters should never be None as they're typically required
|
|
153
|
+
# but we'll handle that case anyway
|
|
142
154
|
path_params = {
|
|
143
155
|
p.name: kwargs.get(p.name)
|
|
144
156
|
for p in self._route.parameters
|
|
145
157
|
if p.location == "path"
|
|
158
|
+
and p.name in kwargs
|
|
159
|
+
and kwargs.get(p.name) is not None
|
|
146
160
|
}
|
|
161
|
+
|
|
162
|
+
# Ensure all path parameters are provided
|
|
163
|
+
required_path_params = {
|
|
164
|
+
p.name
|
|
165
|
+
for p in self._route.parameters
|
|
166
|
+
if p.location == "path" and p.required
|
|
167
|
+
}
|
|
168
|
+
missing_params = required_path_params - path_params.keys()
|
|
169
|
+
if missing_params:
|
|
170
|
+
raise ValueError(f"Missing required path parameters: {missing_params}")
|
|
171
|
+
|
|
147
172
|
for param_name, param_value in path_params.items():
|
|
148
173
|
path = path.replace(f"{{{param_name}}}", str(param_value))
|
|
149
174
|
|
|
150
|
-
# Prepare query parameters
|
|
175
|
+
# Prepare query parameters - filter out None and empty strings
|
|
151
176
|
query_params = {
|
|
152
177
|
p.name: kwargs.get(p.name)
|
|
153
178
|
for p in self._route.parameters
|
|
154
|
-
if p.location == "query"
|
|
179
|
+
if p.location == "query"
|
|
180
|
+
and p.name in kwargs
|
|
181
|
+
and kwargs.get(p.name) is not None
|
|
182
|
+
and kwargs.get(p.name) != ""
|
|
155
183
|
}
|
|
156
184
|
|
|
157
185
|
# Prepare headers - fix typing by ensuring all values are strings
|
|
@@ -198,7 +226,7 @@ class OpenAPITool(Tool):
|
|
|
198
226
|
params=query_params,
|
|
199
227
|
headers=headers,
|
|
200
228
|
json=json_data,
|
|
201
|
-
timeout=
|
|
229
|
+
timeout=self._timeout,
|
|
202
230
|
)
|
|
203
231
|
|
|
204
232
|
# Raise for 4xx/5xx responses
|
|
@@ -229,9 +257,14 @@ class OpenAPITool(Tool):
|
|
|
229
257
|
# Handle request errors (connection, timeout, etc.)
|
|
230
258
|
raise ValueError(f"Request error: {str(e)}")
|
|
231
259
|
|
|
232
|
-
async def run(
|
|
260
|
+
async def run(
|
|
261
|
+
self,
|
|
262
|
+
arguments: dict[str, Any],
|
|
263
|
+
context: Context[ServerSessionT, LifespanContextT] | None = None,
|
|
264
|
+
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
233
265
|
"""Run the tool with arguments and optional context."""
|
|
234
|
-
|
|
266
|
+
response = await self._execute_request(**arguments, context=context)
|
|
267
|
+
return _convert_to_content(response)
|
|
235
268
|
|
|
236
269
|
|
|
237
270
|
class OpenAPIResource(Resource):
|
|
@@ -246,6 +279,7 @@ class OpenAPIResource(Resource):
|
|
|
246
279
|
description: str,
|
|
247
280
|
mime_type: str = "application/json",
|
|
248
281
|
tags: set[str] = set(),
|
|
282
|
+
timeout: float | None = None,
|
|
249
283
|
):
|
|
250
284
|
super().__init__(
|
|
251
285
|
uri=AnyUrl(uri), # Convert string to AnyUrl
|
|
@@ -256,8 +290,11 @@ class OpenAPIResource(Resource):
|
|
|
256
290
|
)
|
|
257
291
|
self._client = client
|
|
258
292
|
self._route = route
|
|
293
|
+
self._timeout = timeout
|
|
259
294
|
|
|
260
|
-
async def read(
|
|
295
|
+
async def read(
|
|
296
|
+
self, context: Context[ServerSessionT, LifespanContextT] | None = None
|
|
297
|
+
) -> str | bytes:
|
|
261
298
|
"""Fetch the resource data by making an HTTP request."""
|
|
262
299
|
try:
|
|
263
300
|
# Extract path parameters from the URI if present
|
|
@@ -268,30 +305,44 @@ class OpenAPIResource(Resource):
|
|
|
268
305
|
if "{" in path and "}" in path:
|
|
269
306
|
# Extract the resource ID from the URI (the last part after the last slash)
|
|
270
307
|
parts = resource_uri.split("/")
|
|
308
|
+
|
|
271
309
|
if len(parts) > 1:
|
|
272
310
|
# Find all path parameters in the route path
|
|
273
311
|
path_params = {}
|
|
274
312
|
|
|
275
|
-
#
|
|
276
|
-
param_value = parts[
|
|
277
|
-
-1
|
|
278
|
-
] # The last part contains the parameter value
|
|
279
|
-
|
|
280
|
-
# Find the path parameter name from the route path
|
|
313
|
+
# Find the path parameter names from the route path
|
|
281
314
|
param_matches = re.findall(r"\{([^}]+)\}", path)
|
|
282
315
|
if param_matches:
|
|
283
|
-
#
|
|
284
|
-
|
|
285
|
-
|
|
316
|
+
# Reverse sorting from creation order (traversal is backwards)
|
|
317
|
+
param_matches.sort(reverse=True)
|
|
318
|
+
# Number of sent parameters is number of parts -1 (assuming first part is resource identifier)
|
|
319
|
+
expected_param_count = len(parts) - 1
|
|
320
|
+
# Map parameters from the end of the URI to the parameters in the path
|
|
321
|
+
# Last parameter in URI (parts[-1]) maps to last parameter in path, and so on
|
|
322
|
+
for i, param_name in enumerate(param_matches):
|
|
323
|
+
# Ensure we don't use resource identifier as parameter
|
|
324
|
+
if i < expected_param_count:
|
|
325
|
+
# Get values from the end of parts
|
|
326
|
+
param_value = parts[-1 - i]
|
|
327
|
+
path_params[param_name] = param_value
|
|
286
328
|
|
|
287
329
|
# Replace path parameters with their values
|
|
288
330
|
for param_name, param_value in path_params.items():
|
|
289
331
|
path = path.replace(f"{{{param_name}}}", str(param_value))
|
|
290
332
|
|
|
333
|
+
# Filter any query parameters - get query parameters and filter out None/empty values
|
|
334
|
+
query_params = {}
|
|
335
|
+
for param in self._route.parameters:
|
|
336
|
+
if param.location == "query" and hasattr(self, f"_{param.name}"):
|
|
337
|
+
value = getattr(self, f"_{param.name}")
|
|
338
|
+
if value is not None and value != "":
|
|
339
|
+
query_params[param.name] = value
|
|
340
|
+
|
|
291
341
|
response = await self._client.request(
|
|
292
342
|
method=self._route.method,
|
|
293
343
|
url=path,
|
|
294
|
-
|
|
344
|
+
params=query_params,
|
|
345
|
+
timeout=self._timeout,
|
|
295
346
|
)
|
|
296
347
|
|
|
297
348
|
# Raise for 4xx/5xx responses
|
|
@@ -339,6 +390,7 @@ class OpenAPIResourceTemplate(ResourceTemplate):
|
|
|
339
390
|
description: str,
|
|
340
391
|
parameters: dict[str, Any],
|
|
341
392
|
tags: set[str] = set(),
|
|
393
|
+
timeout: float | None = None,
|
|
342
394
|
):
|
|
343
395
|
super().__init__(
|
|
344
396
|
uri_template=uri_template,
|
|
@@ -347,11 +399,18 @@ class OpenAPIResourceTemplate(ResourceTemplate):
|
|
|
347
399
|
fn=lambda **kwargs: None,
|
|
348
400
|
parameters=parameters,
|
|
349
401
|
tags=tags,
|
|
402
|
+
context_kwarg=None,
|
|
350
403
|
)
|
|
351
404
|
self._client = client
|
|
352
405
|
self._route = route
|
|
406
|
+
self._timeout = timeout
|
|
353
407
|
|
|
354
|
-
async def create_resource(
|
|
408
|
+
async def create_resource(
|
|
409
|
+
self,
|
|
410
|
+
uri: str,
|
|
411
|
+
params: dict[str, Any],
|
|
412
|
+
context: Context[ServerSessionT, LifespanContextT] | None = None,
|
|
413
|
+
) -> Resource:
|
|
355
414
|
"""Create a resource with the given parameters."""
|
|
356
415
|
# Generate a URI for this resource instance
|
|
357
416
|
uri_parts = []
|
|
@@ -367,6 +426,7 @@ class OpenAPIResourceTemplate(ResourceTemplate):
|
|
|
367
426
|
description=self.description or f"Resource for {self._route.path}",
|
|
368
427
|
mime_type="application/json",
|
|
369
428
|
tags=set(self._route.tags or []),
|
|
429
|
+
timeout=self._timeout,
|
|
370
430
|
)
|
|
371
431
|
|
|
372
432
|
|
|
@@ -414,6 +474,7 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
414
474
|
client: httpx.AsyncClient,
|
|
415
475
|
name: str | None = None,
|
|
416
476
|
route_maps: list[RouteMap] | None = None,
|
|
477
|
+
timeout: float | None = None,
|
|
417
478
|
**settings: Any,
|
|
418
479
|
):
|
|
419
480
|
"""
|
|
@@ -424,13 +485,13 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
424
485
|
client: httpx AsyncClient for making HTTP requests
|
|
425
486
|
name: Optional name for the server
|
|
426
487
|
route_maps: Optional list of RouteMap objects defining route mappings
|
|
427
|
-
|
|
488
|
+
timeout: Optional timeout (in seconds) for all requests
|
|
428
489
|
**settings: Additional settings for FastMCP
|
|
429
490
|
"""
|
|
430
491
|
super().__init__(name=name or "OpenAPI FastMCP", **settings)
|
|
431
492
|
|
|
432
493
|
self._client = client
|
|
433
|
-
|
|
494
|
+
self._timeout = timeout
|
|
434
495
|
http_routes = openapi.parse_openapi_to_http_routes(openapi_spec)
|
|
435
496
|
|
|
436
497
|
# Process routes
|
|
@@ -488,6 +549,7 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
488
549
|
fn_metadata=func_metadata(_openapi_passthrough),
|
|
489
550
|
is_async=True,
|
|
490
551
|
tags=set(route.tags or []),
|
|
552
|
+
timeout=self._timeout,
|
|
491
553
|
)
|
|
492
554
|
# Register the tool by directly assigning to the tools dictionary
|
|
493
555
|
self._tool_manager._tools[tool_name] = tool
|
|
@@ -516,6 +578,7 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
516
578
|
name=resource_name,
|
|
517
579
|
description=enhanced_description,
|
|
518
580
|
tags=set(route.tags or []),
|
|
581
|
+
timeout=self._timeout,
|
|
519
582
|
)
|
|
520
583
|
# Register the resource by directly assigning to the resources dictionary
|
|
521
584
|
self._resource_manager._resources[str(resource.uri)] = resource
|
|
@@ -561,6 +624,7 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
561
624
|
description=enhanced_description,
|
|
562
625
|
parameters=template_params_schema,
|
|
563
626
|
tags=set(route.tags or []),
|
|
627
|
+
timeout=self._timeout,
|
|
564
628
|
)
|
|
565
629
|
# Register the template by directly assigning to the templates dictionary
|
|
566
630
|
self._resource_manager._templates[uri_template_str] = template
|
|
@@ -573,13 +637,4 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
573
637
|
|
|
574
638
|
context = self.get_context()
|
|
575
639
|
result = await self._tool_manager.call_tool(name, arguments, context=context)
|
|
576
|
-
|
|
577
|
-
# For other tools, ensure the response is wrapped in TextContent
|
|
578
|
-
if isinstance(result, dict | str):
|
|
579
|
-
if isinstance(result, dict):
|
|
580
|
-
result_text = json.dumps(result)
|
|
581
|
-
else:
|
|
582
|
-
result_text = result
|
|
583
|
-
return [TextContent(text=result_text, type="text")]
|
|
584
|
-
|
|
585
640
|
return result
|
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,8 +58,10 @@ class ProxyTool(Tool):
|
|
|
50
58
|
)
|
|
51
59
|
|
|
52
60
|
async def run(
|
|
53
|
-
self,
|
|
54
|
-
|
|
61
|
+
self,
|
|
62
|
+
arguments: dict[str, Any],
|
|
63
|
+
context: Context[ServerSessionT, LifespanContextT] | None = None,
|
|
64
|
+
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
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
|
|
57
67
|
async with self._client:
|
|
@@ -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
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""FastMCP - A more ergonomic interface for MCP servers."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import datetime
|
|
4
6
|
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
5
7
|
from contextlib import (
|
|
@@ -63,7 +65,7 @@ class MountedServer:
|
|
|
63
65
|
def __init__(
|
|
64
66
|
self,
|
|
65
67
|
prefix: str,
|
|
66
|
-
server:
|
|
68
|
+
server: FastMCP,
|
|
67
69
|
tool_separator: str | None = None,
|
|
68
70
|
resource_separator: str | None = None,
|
|
69
71
|
prompt_separator: str | None = None,
|
|
@@ -149,7 +151,7 @@ class TimedCache:
|
|
|
149
151
|
|
|
150
152
|
|
|
151
153
|
@asynccontextmanager
|
|
152
|
-
async def default_lifespan(server:
|
|
154
|
+
async def default_lifespan(server: FastMCP) -> AsyncIterator[Any]:
|
|
153
155
|
"""Default lifespan context manager that does nothing.
|
|
154
156
|
|
|
155
157
|
Args:
|
|
@@ -162,8 +164,8 @@ async def default_lifespan(server: "FastMCP") -> AsyncIterator[Any]:
|
|
|
162
164
|
|
|
163
165
|
|
|
164
166
|
def _lifespan_wrapper(
|
|
165
|
-
app:
|
|
166
|
-
lifespan: Callable[[
|
|
167
|
+
app: FastMCP,
|
|
168
|
+
lifespan: Callable[[FastMCP], AbstractAsyncContextManager[LifespanResultT]],
|
|
167
169
|
) -> Callable[
|
|
168
170
|
[MCPServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
|
|
169
171
|
]:
|
|
@@ -182,7 +184,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
182
184
|
name: str | None = None,
|
|
183
185
|
instructions: str | None = None,
|
|
184
186
|
lifespan: (
|
|
185
|
-
Callable[
|
|
187
|
+
Callable[
|
|
188
|
+
[FastMCP[LifespanResultT]],
|
|
189
|
+
AbstractAsyncContextManager[LifespanResultT],
|
|
190
|
+
]
|
|
191
|
+
| None
|
|
186
192
|
) = None,
|
|
187
193
|
tags: set[str] | None = None,
|
|
188
194
|
**settings: Any,
|
|
@@ -273,7 +279,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
273
279
|
self._mcp_server.get_prompt()(self._mcp_get_prompt)
|
|
274
280
|
self._mcp_server.list_resource_templates()(self._mcp_list_resource_templates)
|
|
275
281
|
|
|
276
|
-
def get_context(self) ->
|
|
282
|
+
def get_context(self) -> Context[ServerSession, LifespanResultT]:
|
|
277
283
|
"""
|
|
278
284
|
Returns a Context object. Note that the context will only be valid
|
|
279
285
|
during a request; outside a request, most methods will error.
|
|
@@ -398,9 +404,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
398
404
|
server.
|
|
399
405
|
"""
|
|
400
406
|
if self._resource_manager.has_resource(uri):
|
|
401
|
-
|
|
407
|
+
context = self.get_context()
|
|
408
|
+
resource = await self._resource_manager.get_resource(uri, context=context)
|
|
402
409
|
try:
|
|
403
|
-
content = await resource.read()
|
|
410
|
+
content = await resource.read(context=context)
|
|
404
411
|
return [
|
|
405
412
|
ReadResourceContents(content=content, mime_type=resource.mime_type)
|
|
406
413
|
]
|
|
@@ -424,7 +431,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
424
431
|
|
|
425
432
|
"""
|
|
426
433
|
if self._prompt_manager.has_prompt(name):
|
|
427
|
-
|
|
434
|
+
context = self.get_context()
|
|
435
|
+
messages = await self._prompt_manager.render_prompt(
|
|
436
|
+
name, arguments=arguments or {}, context=context
|
|
437
|
+
)
|
|
428
438
|
return GetPromptResult(messages=pydantic_core.to_jsonable_python(messages))
|
|
429
439
|
else:
|
|
430
440
|
for server in self._mounted_servers.values():
|
|
@@ -562,6 +572,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
562
572
|
- bytes for binary content
|
|
563
573
|
- other types will be converted to JSON
|
|
564
574
|
|
|
575
|
+
Resources can optionally request a Context object by adding a parameter with the
|
|
576
|
+
Context type annotation. The context provides access to MCP capabilities like
|
|
577
|
+
logging, progress reporting, and session information.
|
|
578
|
+
|
|
565
579
|
If the URI contains parameters (e.g. "resource://{param}") or the function
|
|
566
580
|
has parameters, it will be registered as a template resource.
|
|
567
581
|
|
|
@@ -586,6 +600,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
586
600
|
def get_weather(city: str) -> str:
|
|
587
601
|
return f"Weather for {city}"
|
|
588
602
|
|
|
603
|
+
@server.resource("resource://{city}/weather")
|
|
604
|
+
def get_weather_with_context(city: str, ctx: Context) -> str:
|
|
605
|
+
ctx.info(f"Fetching weather for {city}")
|
|
606
|
+
return f"Weather for {city}"
|
|
607
|
+
|
|
589
608
|
@server.resource("resource://{city}/weather")
|
|
590
609
|
async def get_weather(city: str) -> str:
|
|
591
610
|
data = await fetch_weather(city)
|
|
@@ -639,6 +658,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
639
658
|
) -> Callable[[AnyFunction], AnyFunction]:
|
|
640
659
|
"""Decorator to register a prompt.
|
|
641
660
|
|
|
661
|
+
Prompts can optionally request a Context object by adding a parameter with the
|
|
662
|
+
Context type annotation. The context provides access to MCP capabilities like
|
|
663
|
+
logging, progress reporting, and session information.
|
|
664
|
+
|
|
642
665
|
Args:
|
|
643
666
|
name: Optional name for the prompt (defaults to function name)
|
|
644
667
|
description: Optional description of what the prompt does
|
|
@@ -655,6 +678,17 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
655
678
|
}
|
|
656
679
|
]
|
|
657
680
|
|
|
681
|
+
@server.prompt()
|
|
682
|
+
def analyze_with_context(table_name: str, ctx: Context) -> list[Message]:
|
|
683
|
+
ctx.info(f"Analyzing table {table_name}")
|
|
684
|
+
schema = read_table_schema(table_name)
|
|
685
|
+
return [
|
|
686
|
+
{
|
|
687
|
+
"role": "user",
|
|
688
|
+
"content": f"Analyze this schema:\n{schema}"
|
|
689
|
+
}
|
|
690
|
+
]
|
|
691
|
+
|
|
658
692
|
@server.prompt()
|
|
659
693
|
async def analyze_file(path: str) -> list[Message]:
|
|
660
694
|
content = await read_file(path)
|
|
@@ -738,7 +772,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
738
772
|
def mount(
|
|
739
773
|
self,
|
|
740
774
|
prefix: str,
|
|
741
|
-
server:
|
|
775
|
+
server: FastMCP[LifespanResultT],
|
|
742
776
|
tool_separator: str | None = None,
|
|
743
777
|
resource_separator: str | None = None,
|
|
744
778
|
prompt_separator: str | None = None,
|
|
@@ -763,7 +797,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
763
797
|
async def import_server(
|
|
764
798
|
self,
|
|
765
799
|
prefix: str,
|
|
766
|
-
server:
|
|
800
|
+
server: FastMCP[LifespanResultT],
|
|
767
801
|
tool_separator: str | None = None,
|
|
768
802
|
resource_separator: str | None = None,
|
|
769
803
|
prompt_separator: str | None = None,
|
|
@@ -837,7 +871,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
837
871
|
@classmethod
|
|
838
872
|
def from_openapi(
|
|
839
873
|
cls, openapi_spec: dict[str, Any], client: httpx.AsyncClient, **settings: Any
|
|
840
|
-
) ->
|
|
874
|
+
) -> FastMCPOpenAPI:
|
|
841
875
|
"""
|
|
842
876
|
Create a FastMCP server from an OpenAPI specification.
|
|
843
877
|
"""
|
|
@@ -847,8 +881,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
847
881
|
|
|
848
882
|
@classmethod
|
|
849
883
|
def from_fastapi(
|
|
850
|
-
cls, app:
|
|
851
|
-
) ->
|
|
884
|
+
cls, app: Any, name: str | None = None, **settings: Any
|
|
885
|
+
) -> FastMCPOpenAPI:
|
|
852
886
|
"""
|
|
853
887
|
Create a FastMCP server from a FastAPI application.
|
|
854
888
|
"""
|
|
@@ -866,7 +900,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
866
900
|
)
|
|
867
901
|
|
|
868
902
|
@classmethod
|
|
869
|
-
def from_client(cls, client:
|
|
903
|
+
def from_client(cls, client: Client, **settings: Any) -> FastMCPProxy:
|
|
870
904
|
"""
|
|
871
905
|
Create a FastMCP proxy server from a FastMCP client.
|
|
872
906
|
"""
|