fastmcp 0.4.1__py3-none-any.whl → 2.0.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/__init__.py +15 -4
- fastmcp/cli/__init__.py +0 -1
- fastmcp/cli/claude.py +13 -11
- fastmcp/cli/cli.py +61 -41
- fastmcp/client/__init__.py +25 -0
- fastmcp/client/base.py +1 -0
- fastmcp/client/client.py +181 -0
- fastmcp/client/roots.py +75 -0
- fastmcp/client/sampling.py +50 -0
- fastmcp/client/transports.py +411 -0
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/base.py +27 -26
- fastmcp/prompts/prompt_manager.py +50 -12
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/base.py +2 -2
- fastmcp/resources/resource_manager.py +66 -9
- fastmcp/resources/templates.py +15 -10
- fastmcp/resources/types.py +16 -11
- fastmcp/server/__init__.py +5 -0
- fastmcp/server/context.py +222 -0
- fastmcp/server/openapi.py +625 -0
- fastmcp/server/proxy.py +219 -0
- fastmcp/{server.py → server/server.py} +251 -265
- fastmcp/settings.py +73 -0
- fastmcp/tools/base.py +28 -18
- fastmcp/tools/tool_manager.py +45 -10
- fastmcp/utilities/func_metadata.py +33 -19
- fastmcp/utilities/openapi.py +797 -0
- fastmcp/utilities/types.py +3 -4
- fastmcp-2.0.0.dist-info/METADATA +770 -0
- fastmcp-2.0.0.dist-info/RECORD +39 -0
- {fastmcp-0.4.1.dist-info → fastmcp-2.0.0.dist-info}/WHEEL +1 -1
- fastmcp-2.0.0.dist-info/licenses/LICENSE +201 -0
- fastmcp/prompts/manager.py +0 -50
- fastmcp-0.4.1.dist-info/METADATA +0 -587
- fastmcp-0.4.1.dist-info/RECORD +0 -28
- fastmcp-0.4.1.dist-info/licenses/LICENSE +0 -21
- {fastmcp-0.4.1.dist-info → fastmcp-2.0.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,98 +1,92 @@
|
|
|
1
1
|
"""FastMCP - A more ergonomic interface for MCP servers."""
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
|
-
import functools
|
|
5
3
|
import inspect
|
|
6
4
|
import json
|
|
7
5
|
import re
|
|
8
|
-
from
|
|
9
|
-
from
|
|
6
|
+
from collections.abc import AsyncIterator, Callable, Sequence
|
|
7
|
+
from contextlib import (
|
|
8
|
+
AbstractAsyncContextManager,
|
|
9
|
+
asynccontextmanager,
|
|
10
|
+
)
|
|
11
|
+
from typing import TYPE_CHECKING, Any, Generic, Literal
|
|
10
12
|
|
|
13
|
+
import anyio
|
|
14
|
+
import httpx
|
|
11
15
|
import pydantic_core
|
|
12
|
-
from pydantic import Field
|
|
13
16
|
import uvicorn
|
|
14
|
-
from
|
|
17
|
+
from fastapi import FastAPI
|
|
18
|
+
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
|
19
|
+
from mcp.server.lowlevel.server import LifespanResultT
|
|
20
|
+
from mcp.server.lowlevel.server import Server as MCPServer
|
|
21
|
+
from mcp.server.lowlevel.server import lifespan as default_lifespan
|
|
22
|
+
from mcp.server.session import ServerSession
|
|
15
23
|
from mcp.server.sse import SseServerTransport
|
|
16
24
|
from mcp.server.stdio import stdio_server
|
|
17
|
-
from mcp.shared.context import RequestContext
|
|
18
25
|
from mcp.types import (
|
|
26
|
+
AnyFunction,
|
|
19
27
|
EmbeddedResource,
|
|
20
28
|
GetPromptResult,
|
|
21
29
|
ImageContent,
|
|
22
30
|
TextContent,
|
|
23
31
|
)
|
|
24
|
-
from mcp.types import
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
from mcp.types import
|
|
29
|
-
Resource as MCPResource,
|
|
30
|
-
)
|
|
31
|
-
from mcp.types import (
|
|
32
|
-
ResourceTemplate as MCPResourceTemplate,
|
|
33
|
-
)
|
|
34
|
-
from mcp.types import (
|
|
35
|
-
Tool as MCPTool,
|
|
36
|
-
)
|
|
37
|
-
from pydantic import BaseModel
|
|
32
|
+
from mcp.types import Prompt as MCPPrompt
|
|
33
|
+
from mcp.types import PromptArgument as MCPPromptArgument
|
|
34
|
+
from mcp.types import Resource as MCPResource
|
|
35
|
+
from mcp.types import ResourceTemplate as MCPResourceTemplate
|
|
36
|
+
from mcp.types import Tool as MCPTool
|
|
38
37
|
from pydantic.networks import AnyUrl
|
|
39
|
-
from
|
|
38
|
+
from starlette.applications import Starlette
|
|
39
|
+
from starlette.requests import Request
|
|
40
|
+
from starlette.routing import Mount, Route
|
|
40
41
|
|
|
42
|
+
import fastmcp
|
|
43
|
+
import fastmcp.settings
|
|
41
44
|
from fastmcp.exceptions import ResourceError
|
|
42
45
|
from fastmcp.prompts import Prompt, PromptManager
|
|
43
|
-
from fastmcp.prompts.base import PromptResult
|
|
44
46
|
from fastmcp.resources import FunctionResource, Resource, ResourceManager
|
|
45
47
|
from fastmcp.tools import ToolManager
|
|
46
48
|
from fastmcp.utilities.logging import configure_logging, get_logger
|
|
47
49
|
from fastmcp.utilities.types import Image
|
|
48
50
|
|
|
51
|
+
if TYPE_CHECKING:
|
|
52
|
+
from fastmcp.client import Client
|
|
53
|
+
from fastmcp.server.context import Context
|
|
54
|
+
from fastmcp.server.openapi import FastMCPOpenAPI
|
|
55
|
+
from fastmcp.server.proxy import FastMCPProxy
|
|
49
56
|
logger = get_logger(__name__)
|
|
50
57
|
|
|
51
|
-
P = ParamSpec("P")
|
|
52
|
-
R = TypeVar("R")
|
|
53
|
-
R_PromptResult = TypeVar("R_PromptResult", bound=PromptResult)
|
|
54
58
|
|
|
59
|
+
def lifespan_wrapper(
|
|
60
|
+
app: "FastMCP",
|
|
61
|
+
lifespan: Callable[["FastMCP"], AbstractAsyncContextManager[LifespanResultT]],
|
|
62
|
+
) -> Callable[
|
|
63
|
+
[MCPServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
|
|
64
|
+
]:
|
|
65
|
+
@asynccontextmanager
|
|
66
|
+
async def wrap(s: MCPServer[LifespanResultT]) -> AsyncIterator[LifespanResultT]:
|
|
67
|
+
async with lifespan(app) as context:
|
|
68
|
+
yield context
|
|
55
69
|
|
|
56
|
-
|
|
57
|
-
"""FastMCP server settings.
|
|
70
|
+
return wrap
|
|
58
71
|
|
|
59
|
-
All settings can be configured via environment variables with the prefix FASTMCP_.
|
|
60
|
-
For example, FASTMCP_DEBUG=true will set debug=True.
|
|
61
|
-
"""
|
|
62
|
-
|
|
63
|
-
model_config: SettingsConfigDict = SettingsConfigDict(
|
|
64
|
-
env_prefix="FASTMCP_",
|
|
65
|
-
env_file=".env",
|
|
66
|
-
extra="ignore",
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
# Server settings
|
|
70
|
-
debug: bool = False
|
|
71
|
-
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
|
|
72
|
-
|
|
73
|
-
# HTTP settings
|
|
74
|
-
host: str = "0.0.0.0"
|
|
75
|
-
port: int = 8000
|
|
76
|
-
|
|
77
|
-
# resource settings
|
|
78
|
-
warn_on_duplicate_resources: bool = True
|
|
79
|
-
|
|
80
|
-
# tool settings
|
|
81
|
-
warn_on_duplicate_tools: bool = True
|
|
82
|
-
|
|
83
|
-
# prompt settings
|
|
84
|
-
warn_on_duplicate_prompts: bool = True
|
|
85
|
-
|
|
86
|
-
dependencies: list[str] = Field(
|
|
87
|
-
default_factory=list,
|
|
88
|
-
description="List of dependencies to install in the server environment",
|
|
89
|
-
)
|
|
90
72
|
|
|
73
|
+
class FastMCP(Generic[LifespanResultT]):
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
name: str | None = None,
|
|
77
|
+
instructions: str | None = None,
|
|
78
|
+
lifespan: (
|
|
79
|
+
Callable[["FastMCP"], AbstractAsyncContextManager[LifespanResultT]] | None
|
|
80
|
+
) = None,
|
|
81
|
+
**settings: Any,
|
|
82
|
+
):
|
|
83
|
+
self.settings = fastmcp.settings.ServerSettings(**settings)
|
|
91
84
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
85
|
+
self._mcp_server = MCPServer[LifespanResultT](
|
|
86
|
+
name=name or "FastMCP",
|
|
87
|
+
instructions=instructions,
|
|
88
|
+
lifespan=lifespan_wrapper(self, lifespan) if lifespan else default_lifespan, # type: ignore
|
|
89
|
+
)
|
|
96
90
|
self._tool_manager = ToolManager(
|
|
97
91
|
warn_on_duplicate_tools=self.settings.warn_on_duplicate_tools
|
|
98
92
|
)
|
|
@@ -104,6 +98,9 @@ class FastMCP:
|
|
|
104
98
|
)
|
|
105
99
|
self.dependencies = self.settings.dependencies
|
|
106
100
|
|
|
101
|
+
# Setup for mounted apps
|
|
102
|
+
self._mounted_apps: dict[str, FastMCP] = {}
|
|
103
|
+
|
|
107
104
|
# Set up MCP protocol handlers
|
|
108
105
|
self._setup_handlers()
|
|
109
106
|
|
|
@@ -114,20 +111,33 @@ class FastMCP:
|
|
|
114
111
|
def name(self) -> str:
|
|
115
112
|
return self._mcp_server.name
|
|
116
113
|
|
|
117
|
-
|
|
118
|
-
|
|
114
|
+
@property
|
|
115
|
+
def instructions(self) -> str | None:
|
|
116
|
+
return self._mcp_server.instructions
|
|
117
|
+
|
|
118
|
+
async def run_async(self, transport: Literal["stdio", "sse"] | None = None) -> None:
|
|
119
|
+
"""Run the FastMCP server asynchronously.
|
|
119
120
|
|
|
120
121
|
Args:
|
|
121
122
|
transport: Transport protocol to use ("stdio" or "sse")
|
|
122
123
|
"""
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
if transport is None:
|
|
125
|
+
transport = "stdio"
|
|
126
|
+
if transport not in ["stdio", "sse"]:
|
|
125
127
|
raise ValueError(f"Unknown transport: {transport}")
|
|
126
128
|
|
|
127
129
|
if transport == "stdio":
|
|
128
|
-
|
|
130
|
+
await self.run_stdio_async()
|
|
129
131
|
else: # transport == "sse"
|
|
130
|
-
|
|
132
|
+
await self.run_sse_async()
|
|
133
|
+
|
|
134
|
+
def run(self, transport: Literal["stdio", "sse"] | None = None) -> None:
|
|
135
|
+
"""Run the FastMCP server. Note this is a synchronous function.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
transport: Transport protocol to use ("stdio" or "sse")
|
|
139
|
+
"""
|
|
140
|
+
anyio.run(self.run_async, transport)
|
|
131
141
|
|
|
132
142
|
def _setup_handlers(self) -> None:
|
|
133
143
|
"""Set up core MCP protocol handlers."""
|
|
@@ -137,8 +147,7 @@ class FastMCP:
|
|
|
137
147
|
self._mcp_server.read_resource()(self.read_resource)
|
|
138
148
|
self._mcp_server.list_prompts()(self.list_prompts)
|
|
139
149
|
self._mcp_server.get_prompt()(self.get_prompt)
|
|
140
|
-
|
|
141
|
-
# self._mcp_server.list_resource_templates()(self.list_resource_templates)
|
|
150
|
+
self._mcp_server.list_resource_templates()(self.list_resource_templates)
|
|
142
151
|
|
|
143
152
|
async def list_tools(self) -> list[MCPTool]:
|
|
144
153
|
"""List all available tools."""
|
|
@@ -152,19 +161,22 @@ class FastMCP:
|
|
|
152
161
|
for info in tools
|
|
153
162
|
]
|
|
154
163
|
|
|
155
|
-
def get_context(self) -> "Context":
|
|
164
|
+
def get_context(self) -> "Context[ServerSession, LifespanResultT]":
|
|
156
165
|
"""
|
|
157
166
|
Returns a Context object. Note that the context will only be valid
|
|
158
167
|
during a request; outside a request, most methods will error.
|
|
159
168
|
"""
|
|
169
|
+
|
|
160
170
|
try:
|
|
161
171
|
request_context = self._mcp_server.request_context
|
|
162
172
|
except LookupError:
|
|
163
173
|
request_context = None
|
|
174
|
+
from fastmcp.server.context import Context
|
|
175
|
+
|
|
164
176
|
return Context(request_context=request_context, fastmcp=self)
|
|
165
177
|
|
|
166
178
|
async def call_tool(
|
|
167
|
-
self, name: str, arguments: dict
|
|
179
|
+
self, name: str, arguments: dict[str, Any]
|
|
168
180
|
) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
|
|
169
181
|
"""Call a tool by name with arguments."""
|
|
170
182
|
context = self.get_context()
|
|
@@ -197,21 +209,23 @@ class FastMCP:
|
|
|
197
209
|
for template in templates
|
|
198
210
|
]
|
|
199
211
|
|
|
200
|
-
async def read_resource(self, uri: AnyUrl | str) ->
|
|
212
|
+
async def read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
|
|
201
213
|
"""Read a resource by URI."""
|
|
214
|
+
|
|
202
215
|
resource = await self._resource_manager.get_resource(uri)
|
|
203
216
|
if not resource:
|
|
204
217
|
raise ResourceError(f"Unknown resource: {uri}")
|
|
205
218
|
|
|
206
219
|
try:
|
|
207
|
-
|
|
220
|
+
content = await resource.read()
|
|
221
|
+
return [ReadResourceContents(content=content, mime_type=resource.mime_type)]
|
|
208
222
|
except Exception as e:
|
|
209
223
|
logger.error(f"Error reading resource {uri}: {e}")
|
|
210
224
|
raise ResourceError(str(e))
|
|
211
225
|
|
|
212
226
|
def add_tool(
|
|
213
227
|
self,
|
|
214
|
-
fn:
|
|
228
|
+
fn: AnyFunction,
|
|
215
229
|
name: str | None = None,
|
|
216
230
|
description: str | None = None,
|
|
217
231
|
) -> None:
|
|
@@ -229,11 +243,12 @@ class FastMCP:
|
|
|
229
243
|
|
|
230
244
|
def tool(
|
|
231
245
|
self, name: str | None = None, description: str | None = None
|
|
232
|
-
) -> Callable[[
|
|
246
|
+
) -> Callable[[AnyFunction], AnyFunction]:
|
|
233
247
|
"""Decorator to register a tool.
|
|
234
248
|
|
|
235
|
-
Tools can optionally request a Context object by adding a parameter with the
|
|
236
|
-
The context provides access to MCP capabilities like
|
|
249
|
+
Tools can optionally request a Context object by adding a parameter with the
|
|
250
|
+
Context type annotation. The context provides access to MCP capabilities like
|
|
251
|
+
logging, progress reporting, and resource access.
|
|
237
252
|
|
|
238
253
|
Args:
|
|
239
254
|
name: Optional name for the tool (defaults to function name)
|
|
@@ -261,7 +276,7 @@ class FastMCP:
|
|
|
261
276
|
"Did you forget to call it? Use @tool() instead of @tool"
|
|
262
277
|
)
|
|
263
278
|
|
|
264
|
-
def decorator(fn:
|
|
279
|
+
def decorator(fn: AnyFunction) -> AnyFunction:
|
|
265
280
|
self.add_tool(fn, name=name, description=description)
|
|
266
281
|
return fn
|
|
267
282
|
|
|
@@ -282,7 +297,7 @@ class FastMCP:
|
|
|
282
297
|
name: str | None = None,
|
|
283
298
|
description: str | None = None,
|
|
284
299
|
mime_type: str | None = None,
|
|
285
|
-
) -> Callable[[
|
|
300
|
+
) -> Callable[[AnyFunction], AnyFunction]:
|
|
286
301
|
"""Decorator to register a function as a resource.
|
|
287
302
|
|
|
288
303
|
The function will be called when the resource is read to generate its content.
|
|
@@ -305,9 +320,19 @@ class FastMCP:
|
|
|
305
320
|
def get_data() -> str:
|
|
306
321
|
return "Hello, world!"
|
|
307
322
|
|
|
323
|
+
@server.resource("resource://my-resource")
|
|
324
|
+
async get_data() -> str:
|
|
325
|
+
data = await fetch_data()
|
|
326
|
+
return f"Hello, world! {data}"
|
|
327
|
+
|
|
308
328
|
@server.resource("resource://{city}/weather")
|
|
309
329
|
def get_weather(city: str) -> str:
|
|
310
330
|
return f"Weather for {city}"
|
|
331
|
+
|
|
332
|
+
@server.resource("resource://{city}/weather")
|
|
333
|
+
async def get_weather(city: str) -> str:
|
|
334
|
+
data = await fetch_weather(city)
|
|
335
|
+
return f"Weather for {city}: {data}"
|
|
311
336
|
"""
|
|
312
337
|
# Check if user passed function directly instead of calling decorator
|
|
313
338
|
if callable(uri):
|
|
@@ -316,11 +341,7 @@ class FastMCP:
|
|
|
316
341
|
"Did you forget to call it? Use @resource('uri') instead of @resource"
|
|
317
342
|
)
|
|
318
343
|
|
|
319
|
-
def decorator(fn:
|
|
320
|
-
@functools.wraps(fn)
|
|
321
|
-
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
322
|
-
return fn(*args, **kwargs)
|
|
323
|
-
|
|
344
|
+
def decorator(fn: AnyFunction) -> AnyFunction:
|
|
324
345
|
# Check if this should be a template
|
|
325
346
|
has_uri_params = "{" in uri and "}" in uri
|
|
326
347
|
has_func_params = bool(inspect.signature(fn).parameters)
|
|
@@ -338,7 +359,7 @@ class FastMCP:
|
|
|
338
359
|
|
|
339
360
|
# Register as template
|
|
340
361
|
self._resource_manager.add_template(
|
|
341
|
-
|
|
362
|
+
fn=fn,
|
|
342
363
|
uri_template=uri,
|
|
343
364
|
name=name,
|
|
344
365
|
description=description,
|
|
@@ -351,10 +372,10 @@ class FastMCP:
|
|
|
351
372
|
name=name,
|
|
352
373
|
description=description,
|
|
353
374
|
mime_type=mime_type or "text/plain",
|
|
354
|
-
fn=
|
|
375
|
+
fn=fn,
|
|
355
376
|
)
|
|
356
377
|
self.add_resource(resource)
|
|
357
|
-
return
|
|
378
|
+
return fn
|
|
358
379
|
|
|
359
380
|
return decorator
|
|
360
381
|
|
|
@@ -368,7 +389,7 @@ class FastMCP:
|
|
|
368
389
|
|
|
369
390
|
def prompt(
|
|
370
391
|
self, name: str | None = None, description: str | None = None
|
|
371
|
-
) -> Callable[[
|
|
392
|
+
) -> Callable[[AnyFunction], AnyFunction]:
|
|
372
393
|
"""Decorator to register a prompt.
|
|
373
394
|
|
|
374
395
|
Args:
|
|
@@ -409,7 +430,7 @@ class FastMCP:
|
|
|
409
430
|
"Did you forget to call it? Use @prompt() instead of @prompt"
|
|
410
431
|
)
|
|
411
432
|
|
|
412
|
-
def decorator(func:
|
|
433
|
+
def decorator(func: AnyFunction) -> AnyFunction:
|
|
413
434
|
prompt = Prompt.from_function(func, name=name, description=description)
|
|
414
435
|
self.add_prompt(prompt)
|
|
415
436
|
return func
|
|
@@ -427,14 +448,26 @@ class FastMCP:
|
|
|
427
448
|
|
|
428
449
|
async def run_sse_async(self) -> None:
|
|
429
450
|
"""Run the server using SSE transport."""
|
|
430
|
-
|
|
431
|
-
|
|
451
|
+
starlette_app = self.sse_app()
|
|
452
|
+
|
|
453
|
+
config = uvicorn.Config(
|
|
454
|
+
starlette_app,
|
|
455
|
+
host=self.settings.host,
|
|
456
|
+
port=self.settings.port,
|
|
457
|
+
log_level=self.settings.log_level.lower(),
|
|
458
|
+
)
|
|
459
|
+
server = uvicorn.Server(config)
|
|
460
|
+
await server.serve()
|
|
432
461
|
|
|
433
|
-
|
|
462
|
+
def sse_app(self) -> Starlette:
|
|
463
|
+
"""Return an instance of the SSE server app."""
|
|
464
|
+
sse = SseServerTransport(self.settings.message_path)
|
|
434
465
|
|
|
435
|
-
async def handle_sse(request):
|
|
466
|
+
async def handle_sse(request: Request) -> None:
|
|
436
467
|
async with sse.connect_sse(
|
|
437
|
-
request.scope,
|
|
468
|
+
request.scope,
|
|
469
|
+
request.receive,
|
|
470
|
+
request._send, # type: ignore[reportPrivateUsage]
|
|
438
471
|
) as streams:
|
|
439
472
|
await self._mcp_server.run(
|
|
440
473
|
streams[0],
|
|
@@ -442,26 +475,14 @@ class FastMCP:
|
|
|
442
475
|
self._mcp_server.create_initialization_options(),
|
|
443
476
|
)
|
|
444
477
|
|
|
445
|
-
|
|
446
|
-
await sse.handle_post_message(request.scope, request.receive, request._send)
|
|
447
|
-
|
|
448
|
-
starlette_app = Starlette(
|
|
478
|
+
return Starlette(
|
|
449
479
|
debug=self.settings.debug,
|
|
450
480
|
routes=[
|
|
451
|
-
Route(
|
|
452
|
-
|
|
481
|
+
Route(self.settings.sse_path, endpoint=handle_sse),
|
|
482
|
+
Mount(self.settings.message_path, app=sse.handle_post_message),
|
|
453
483
|
],
|
|
454
484
|
)
|
|
455
485
|
|
|
456
|
-
config = uvicorn.Config(
|
|
457
|
-
starlette_app,
|
|
458
|
-
host=self.settings.host,
|
|
459
|
-
port=self.settings.port,
|
|
460
|
-
log_level=self.settings.log_level.lower(),
|
|
461
|
-
)
|
|
462
|
-
server = uvicorn.Server(config)
|
|
463
|
-
await server.serve()
|
|
464
|
-
|
|
465
486
|
async def list_prompts(self) -> list[MCPPrompt]:
|
|
466
487
|
"""List all available prompts."""
|
|
467
488
|
prompts = self._prompt_manager.list_prompts()
|
|
@@ -482,7 +503,7 @@ class FastMCP:
|
|
|
482
503
|
]
|
|
483
504
|
|
|
484
505
|
async def get_prompt(
|
|
485
|
-
self, name: str, arguments:
|
|
506
|
+
self, name: str, arguments: dict[str, Any] | None = None
|
|
486
507
|
) -> GetPromptResult:
|
|
487
508
|
"""Get a prompt by name with arguments."""
|
|
488
509
|
try:
|
|
@@ -493,182 +514,147 @@ class FastMCP:
|
|
|
493
514
|
logger.error(f"Error getting prompt {name}: {e}")
|
|
494
515
|
raise ValueError(str(e))
|
|
495
516
|
|
|
517
|
+
def mount(self, prefix: str, app: "FastMCP") -> None:
|
|
518
|
+
"""Mount another FastMCP application with a given prefix.
|
|
496
519
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
if isinstance(result, Image):
|
|
508
|
-
return [result.to_image_content()]
|
|
509
|
-
|
|
510
|
-
if isinstance(result, (list, tuple)):
|
|
511
|
-
return list(chain.from_iterable(_convert_to_content(item) for item in result))
|
|
512
|
-
|
|
513
|
-
if not isinstance(result, str):
|
|
514
|
-
try:
|
|
515
|
-
result = json.dumps(pydantic_core.to_jsonable_python(result))
|
|
516
|
-
except Exception:
|
|
517
|
-
result = str(result)
|
|
518
|
-
|
|
519
|
-
return [TextContent(type="text", text=result)]
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
class Context(BaseModel):
|
|
523
|
-
"""Context object providing access to MCP capabilities.
|
|
524
|
-
|
|
525
|
-
This provides a cleaner interface to MCP's RequestContext functionality.
|
|
526
|
-
It gets injected into tool and resource functions that request it via type hints.
|
|
527
|
-
|
|
528
|
-
To use context in a tool function, add a parameter with the Context type annotation:
|
|
520
|
+
When an application is mounted:
|
|
521
|
+
- The tools are imported with prefixed names
|
|
522
|
+
Example: If app has a tool named "get_weather", it will be available as "weather/get_weather"
|
|
523
|
+
- The resources are imported with prefixed URIs
|
|
524
|
+
Example: If app has a resource with URI "weather://forecast", it will be available as "weather+weather://forecast"
|
|
525
|
+
- The templates are imported with prefixed URI templates
|
|
526
|
+
Example: If app has a template with URI "weather://location/{id}", it will be available as "weather+weather://location/{id}"
|
|
527
|
+
- The prompts are imported with prefixed names
|
|
528
|
+
Example: If app has a prompt named "weather_prompt", it will be available as "weather/weather_prompt"
|
|
529
529
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
530
|
+
Args:
|
|
531
|
+
prefix: The prefix to use for the mounted application
|
|
532
|
+
app: The FastMCP application to mount
|
|
533
|
+
"""
|
|
534
|
+
# Mount the app in the list of mounted apps
|
|
535
|
+
self._mounted_apps[prefix] = app
|
|
536
|
+
|
|
537
|
+
# Import tools from the mounted app with / delimiter
|
|
538
|
+
tool_prefix = f"{prefix}/"
|
|
539
|
+
self._tool_manager.import_tools(app._tool_manager, tool_prefix)
|
|
540
|
+
|
|
541
|
+
# Import resources and templates from the mounted app with + delimiter
|
|
542
|
+
resource_prefix = f"{prefix}+"
|
|
543
|
+
self._resource_manager.import_resources(app._resource_manager, resource_prefix)
|
|
544
|
+
self._resource_manager.import_templates(app._resource_manager, resource_prefix)
|
|
545
|
+
|
|
546
|
+
# Import prompts with / delimiter
|
|
547
|
+
prompt_prefix = f"{prefix}/"
|
|
548
|
+
self._prompt_manager.import_prompts(app._prompt_manager, prompt_prefix)
|
|
549
|
+
|
|
550
|
+
logger.info(f"Mounted app with prefix '{prefix}'")
|
|
551
|
+
logger.debug(f"Imported tools with prefix '{tool_prefix}'")
|
|
552
|
+
logger.debug(f"Imported resources with prefix '{resource_prefix}'")
|
|
553
|
+
logger.debug(f"Imported templates with prefix '{resource_prefix}'")
|
|
554
|
+
logger.debug(f"Imported prompts with prefix '{prompt_prefix}'")
|
|
555
|
+
|
|
556
|
+
@classmethod
|
|
557
|
+
async def as_proxy(
|
|
558
|
+
cls, client: "Client | FastMCP", **settings: Any
|
|
559
|
+
) -> "FastMCPProxy":
|
|
560
|
+
"""
|
|
561
|
+
Create a FastMCP proxy server from a client.
|
|
538
562
|
|
|
539
|
-
|
|
540
|
-
|
|
563
|
+
This method creates a new FastMCP server instance that proxies requests to the provided client.
|
|
564
|
+
It discovers the client's tools, resources, prompts, and templates, and creates corresponding
|
|
565
|
+
components in the server that forward requests to the client.
|
|
541
566
|
|
|
542
|
-
|
|
543
|
-
|
|
567
|
+
Args:
|
|
568
|
+
client: The client to proxy requests to
|
|
569
|
+
**settings: Additional settings for the FastMCP server
|
|
544
570
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
571
|
+
Returns:
|
|
572
|
+
A FastMCP server that proxies requests to the client
|
|
573
|
+
"""
|
|
574
|
+
from fastmcp.client import Client
|
|
548
575
|
|
|
549
|
-
|
|
550
|
-
```
|
|
576
|
+
from .proxy import FastMCPProxy
|
|
551
577
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
"""
|
|
578
|
+
if isinstance(client, Client):
|
|
579
|
+
return await FastMCPProxy.from_client(client=client, **settings)
|
|
555
580
|
|
|
556
|
-
|
|
557
|
-
|
|
581
|
+
elif isinstance(client, FastMCP):
|
|
582
|
+
return await FastMCPProxy.from_server(server=client, **settings)
|
|
558
583
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
*,
|
|
562
|
-
request_context: RequestContext | None = None,
|
|
563
|
-
fastmcp: FastMCP | None = None,
|
|
564
|
-
**kwargs: Any,
|
|
565
|
-
):
|
|
566
|
-
super().__init__(**kwargs)
|
|
567
|
-
self._request_context = request_context
|
|
568
|
-
self._fastmcp = fastmcp
|
|
584
|
+
else:
|
|
585
|
+
raise ValueError(f"Unknown client type: {type(client)}")
|
|
569
586
|
|
|
570
|
-
@
|
|
571
|
-
def
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
587
|
+
@classmethod
|
|
588
|
+
def from_openapi(
|
|
589
|
+
cls, openapi_spec: dict[str, Any], client: httpx.AsyncClient, **settings: Any
|
|
590
|
+
) -> "FastMCPOpenAPI":
|
|
591
|
+
"""
|
|
592
|
+
Create a FastMCP server from an OpenAPI specification.
|
|
593
|
+
"""
|
|
594
|
+
from .openapi import FastMCPOpenAPI
|
|
576
595
|
|
|
577
|
-
|
|
578
|
-
def request_context(self) -> RequestContext:
|
|
579
|
-
"""Access to the underlying request context."""
|
|
580
|
-
if self._request_context is None:
|
|
581
|
-
raise ValueError("Context is not available outside of a request")
|
|
582
|
-
return self._request_context
|
|
583
|
-
|
|
584
|
-
async def report_progress(
|
|
585
|
-
self, progress: float, total: float | None = None
|
|
586
|
-
) -> None:
|
|
587
|
-
"""Report progress for the current operation.
|
|
596
|
+
return FastMCPOpenAPI(openapi_spec=openapi_spec, client=client, **settings)
|
|
588
597
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
598
|
+
@classmethod
|
|
599
|
+
def from_fastapi(
|
|
600
|
+
cls, app: FastAPI, name: str | None = None, **settings: Any
|
|
601
|
+
) -> "FastMCPOpenAPI":
|
|
592
602
|
"""
|
|
603
|
+
Create a FastMCP server from a FastAPI application.
|
|
604
|
+
"""
|
|
605
|
+
from .openapi import FastMCPOpenAPI
|
|
593
606
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
if self.request_context.meta
|
|
597
|
-
else None
|
|
607
|
+
client = httpx.AsyncClient(
|
|
608
|
+
transport=httpx.ASGITransport(app=app), base_url="http://fastapi"
|
|
598
609
|
)
|
|
599
610
|
|
|
600
|
-
|
|
601
|
-
return
|
|
611
|
+
name = name or app.title
|
|
602
612
|
|
|
603
|
-
|
|
604
|
-
|
|
613
|
+
return FastMCPOpenAPI(
|
|
614
|
+
openapi_spec=app.openapi(), client=client, name=name, **settings
|
|
605
615
|
)
|
|
606
616
|
|
|
607
|
-
async def read_resource(self, uri: str | AnyUrl) -> str | bytes:
|
|
608
|
-
"""Read a resource by URI.
|
|
609
617
|
|
|
610
|
-
|
|
611
|
-
|
|
618
|
+
def _convert_to_content(
|
|
619
|
+
result: Any,
|
|
620
|
+
_process_as_single_item: bool = False,
|
|
621
|
+
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
622
|
+
"""Convert a result to a sequence of content objects."""
|
|
623
|
+
if result is None:
|
|
624
|
+
return []
|
|
612
625
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
"""
|
|
616
|
-
assert (
|
|
617
|
-
self._fastmcp is not None
|
|
618
|
-
), "Context is not available outside of a request"
|
|
619
|
-
return await self._fastmcp.read_resource(uri)
|
|
626
|
+
if isinstance(result, TextContent | ImageContent | EmbeddedResource):
|
|
627
|
+
return [result]
|
|
620
628
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
level: Literal["debug", "info", "warning", "error"],
|
|
624
|
-
message: str,
|
|
625
|
-
*,
|
|
626
|
-
logger_name: str | None = None,
|
|
627
|
-
) -> None:
|
|
628
|
-
"""Send a log message to the client.
|
|
629
|
+
if isinstance(result, Image):
|
|
630
|
+
return [result.to_image_content()]
|
|
629
631
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
self.request_context.session.send_log_message(
|
|
637
|
-
level=level, data=message, logger=logger_name
|
|
638
|
-
)
|
|
632
|
+
if isinstance(result, list | tuple) and not _process_as_single_item:
|
|
633
|
+
# if the result is a list, then it could either be a list of MCP types,
|
|
634
|
+
# or a "regular" list that the tool is returning, or a mix of both.
|
|
635
|
+
#
|
|
636
|
+
# so we extract all the MCP types / images and convert them as individual content elements,
|
|
637
|
+
# and aggregate the rest as a single content element
|
|
639
638
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
"""Get the client ID if available."""
|
|
643
|
-
return (
|
|
644
|
-
getattr(self.request_context.meta, "client_id", None)
|
|
645
|
-
if self.request_context.meta
|
|
646
|
-
else None
|
|
647
|
-
)
|
|
639
|
+
mcp_types = []
|
|
640
|
+
other_content = []
|
|
648
641
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
642
|
+
for item in result:
|
|
643
|
+
if isinstance(item, TextContent | ImageContent | EmbeddedResource | Image):
|
|
644
|
+
mcp_types.append(_convert_to_content(item)[0])
|
|
645
|
+
else:
|
|
646
|
+
other_content.append(item)
|
|
647
|
+
if other_content:
|
|
648
|
+
other_content = _convert_to_content(
|
|
649
|
+
other_content, _process_as_single_item=True
|
|
650
|
+
)
|
|
653
651
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
def info(self, message: str, **extra: Any) -> None:
|
|
665
|
-
"""Send an info log message."""
|
|
666
|
-
self.log("info", message, **extra)
|
|
667
|
-
|
|
668
|
-
def warning(self, message: str, **extra: Any) -> None:
|
|
669
|
-
"""Send a warning log message."""
|
|
670
|
-
self.log("warning", message, **extra)
|
|
671
|
-
|
|
672
|
-
def error(self, message: str, **extra: Any) -> None:
|
|
673
|
-
"""Send an error log message."""
|
|
674
|
-
self.log("error", message, **extra)
|
|
652
|
+
return other_content + mcp_types
|
|
653
|
+
|
|
654
|
+
if not isinstance(result, str):
|
|
655
|
+
try:
|
|
656
|
+
result = json.dumps(pydantic_core.to_jsonable_python(result))
|
|
657
|
+
except Exception:
|
|
658
|
+
result = str(result)
|
|
659
|
+
|
|
660
|
+
return [TextContent(type="text", text=result)]
|