fastmcp 2.2.6__py3-none-any.whl → 2.2.8__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/client/client.py +246 -43
- fastmcp/contrib/bulk_tool_caller/bulk_tool_caller.py +1 -3
- fastmcp/prompts/__init__.py +7 -2
- fastmcp/prompts/prompt.py +55 -53
- fastmcp/prompts/prompt_manager.py +10 -3
- fastmcp/resources/template.py +29 -18
- fastmcp/resources/types.py +4 -7
- fastmcp/server/context.py +12 -1
- fastmcp/server/openapi.py +28 -12
- fastmcp/server/proxy.py +7 -9
- fastmcp/server/server.py +243 -19
- fastmcp/settings.py +7 -0
- fastmcp/tools/tool.py +79 -62
- fastmcp/tools/tool_manager.py +16 -3
- fastmcp/utilities/http.py +44 -0
- fastmcp/utilities/json_schema.py +59 -0
- fastmcp/utilities/openapi.py +147 -36
- fastmcp/utilities/types.py +66 -1
- fastmcp-2.2.8.dist-info/METADATA +407 -0
- {fastmcp-2.2.6.dist-info → fastmcp-2.2.8.dist-info}/RECORD +23 -22
- fastmcp/utilities/func_metadata.py +0 -229
- fastmcp-2.2.6.dist-info/METADATA +0 -810
- {fastmcp-2.2.6.dist-info → fastmcp-2.2.8.dist-info}/WHEEL +0 -0
- {fastmcp-2.2.6.dist-info → fastmcp-2.2.8.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.2.6.dist-info → fastmcp-2.2.8.dist-info}/licenses/LICENSE +0 -0
fastmcp/client/client.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
from contextlib import AbstractAsyncContextManager
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Any,
|
|
4
|
+
from typing import Any, cast
|
|
5
5
|
|
|
6
6
|
import mcp.types
|
|
7
7
|
from mcp import ClientSession
|
|
@@ -107,6 +107,7 @@ class Client:
|
|
|
107
107
|
self._session = None
|
|
108
108
|
|
|
109
109
|
# --- MCP Client Methods ---
|
|
110
|
+
|
|
110
111
|
async def ping(self) -> None:
|
|
111
112
|
"""Send a ping request."""
|
|
112
113
|
await self.session.send_ping()
|
|
@@ -128,23 +129,100 @@ class Client:
|
|
|
128
129
|
"""Send a roots/list_changed notification."""
|
|
129
130
|
await self.session.send_roots_list_changed()
|
|
130
131
|
|
|
131
|
-
|
|
132
|
-
|
|
132
|
+
# --- Resources ---
|
|
133
|
+
|
|
134
|
+
async def list_resources_mcp(self) -> mcp.types.ListResourcesResult:
|
|
135
|
+
"""Send a resources/list request and return the complete MCP protocol result.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
mcp.types.ListResourcesResult: The complete response object from the protocol,
|
|
139
|
+
containing the list of resources and any additional metadata.
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
RuntimeError: If called while the client is not connected.
|
|
143
|
+
"""
|
|
133
144
|
result = await self.session.list_resources()
|
|
145
|
+
return result
|
|
146
|
+
|
|
147
|
+
async def list_resources(self) -> list[mcp.types.Resource]:
|
|
148
|
+
"""Retrieve a list of resources available on the server.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
list[mcp.types.Resource]: A list of Resource objects.
|
|
152
|
+
|
|
153
|
+
Raises:
|
|
154
|
+
RuntimeError: If called while the client is not connected.
|
|
155
|
+
"""
|
|
156
|
+
result = await self.list_resources_mcp()
|
|
134
157
|
return result.resources
|
|
135
158
|
|
|
136
|
-
async def
|
|
137
|
-
|
|
159
|
+
async def list_resource_templates_mcp(
|
|
160
|
+
self,
|
|
161
|
+
) -> mcp.types.ListResourceTemplatesResult:
|
|
162
|
+
"""Send a resources/listResourceTemplates request and return the complete MCP protocol result.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
mcp.types.ListResourceTemplatesResult: The complete response object from the protocol,
|
|
166
|
+
containing the list of resource templates and any additional metadata.
|
|
167
|
+
|
|
168
|
+
Raises:
|
|
169
|
+
RuntimeError: If called while the client is not connected.
|
|
170
|
+
"""
|
|
138
171
|
result = await self.session.list_resource_templates()
|
|
172
|
+
return result
|
|
173
|
+
|
|
174
|
+
async def list_resource_templates(
|
|
175
|
+
self,
|
|
176
|
+
) -> list[mcp.types.ResourceTemplate]:
|
|
177
|
+
"""Retrieve a list of resource templates available on the server.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
list[mcp.types.ResourceTemplate]: A list of ResourceTemplate objects.
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
RuntimeError: If called while the client is not connected.
|
|
184
|
+
"""
|
|
185
|
+
result = await self.list_resource_templates_mcp()
|
|
139
186
|
return result.resourceTemplates
|
|
140
187
|
|
|
188
|
+
async def read_resource_mcp(
|
|
189
|
+
self, uri: AnyUrl | str
|
|
190
|
+
) -> mcp.types.ReadResourceResult:
|
|
191
|
+
"""Send a resources/read request and return the complete MCP protocol result.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
uri (AnyUrl | str): The URI of the resource to read. Can be a string or an AnyUrl object.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
mcp.types.ReadResourceResult: The complete response object from the protocol,
|
|
198
|
+
containing the resource contents and any additional metadata.
|
|
199
|
+
|
|
200
|
+
Raises:
|
|
201
|
+
RuntimeError: If called while the client is not connected.
|
|
202
|
+
"""
|
|
203
|
+
if isinstance(uri, str):
|
|
204
|
+
uri = AnyUrl(uri) # Ensure AnyUrl
|
|
205
|
+
result = await self.session.read_resource(uri)
|
|
206
|
+
return result
|
|
207
|
+
|
|
141
208
|
async def read_resource(
|
|
142
209
|
self, uri: AnyUrl | str
|
|
143
210
|
) -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]:
|
|
144
|
-
"""
|
|
211
|
+
"""Read the contents of a resource or resolved template.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
uri (AnyUrl | str): The URI of the resource to read. Can be a string or an AnyUrl object.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]: A list of content
|
|
218
|
+
objects, typically containing either text or binary data.
|
|
219
|
+
|
|
220
|
+
Raises:
|
|
221
|
+
RuntimeError: If called while the client is not connected.
|
|
222
|
+
"""
|
|
145
223
|
if isinstance(uri, str):
|
|
146
224
|
uri = AnyUrl(uri) # Ensure AnyUrl
|
|
147
|
-
result = await self.
|
|
225
|
+
result = await self.read_resource_mcp(uri)
|
|
148
226
|
return result.contents
|
|
149
227
|
|
|
150
228
|
# async def subscribe_resource(self, uri: AnyUrl | str) -> None:
|
|
@@ -159,66 +237,191 @@ class Client:
|
|
|
159
237
|
# uri = AnyUrl(uri)
|
|
160
238
|
# await self.session.unsubscribe_resource(uri)
|
|
161
239
|
|
|
162
|
-
|
|
163
|
-
|
|
240
|
+
# --- Prompts ---
|
|
241
|
+
|
|
242
|
+
async def list_prompts_mcp(self) -> mcp.types.ListPromptsResult:
|
|
243
|
+
"""Send a prompts/list request and return the complete MCP protocol result.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
mcp.types.ListPromptsResult: The complete response object from the protocol,
|
|
247
|
+
containing the list of prompts and any additional metadata.
|
|
248
|
+
|
|
249
|
+
Raises:
|
|
250
|
+
RuntimeError: If called while the client is not connected.
|
|
251
|
+
"""
|
|
164
252
|
result = await self.session.list_prompts()
|
|
253
|
+
return result
|
|
254
|
+
|
|
255
|
+
async def list_prompts(self) -> list[mcp.types.Prompt]:
|
|
256
|
+
"""Retrieve a list of prompts available on the server.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
list[mcp.types.Prompt]: A list of Prompt objects.
|
|
260
|
+
|
|
261
|
+
Raises:
|
|
262
|
+
RuntimeError: If called while the client is not connected.
|
|
263
|
+
"""
|
|
264
|
+
result = await self.list_prompts_mcp()
|
|
165
265
|
return result.prompts
|
|
166
266
|
|
|
267
|
+
# --- Prompt ---
|
|
268
|
+
async def get_prompt_mcp(
|
|
269
|
+
self, name: str, arguments: dict[str, str] | None = None
|
|
270
|
+
) -> mcp.types.GetPromptResult:
|
|
271
|
+
"""Send a prompts/get request and return the complete MCP protocol result.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
name (str): The name of the prompt to retrieve.
|
|
275
|
+
arguments (dict[str, str] | None, optional): Arguments to pass to the prompt. Defaults to None.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
mcp.types.GetPromptResult: The complete response object from the protocol,
|
|
279
|
+
containing the prompt messages and any additional metadata.
|
|
280
|
+
|
|
281
|
+
Raises:
|
|
282
|
+
RuntimeError: If called while the client is not connected.
|
|
283
|
+
"""
|
|
284
|
+
result = await self.session.get_prompt(name=name, arguments=arguments)
|
|
285
|
+
return result
|
|
286
|
+
|
|
167
287
|
async def get_prompt(
|
|
168
288
|
self, name: str, arguments: dict[str, str] | None = None
|
|
169
|
-
) ->
|
|
170
|
-
"""
|
|
171
|
-
|
|
172
|
-
|
|
289
|
+
) -> mcp.types.GetPromptResult:
|
|
290
|
+
"""Retrieve a rendered prompt message list from the server.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
name (str): The name of the prompt to retrieve.
|
|
294
|
+
arguments (dict[str, str] | None, optional): Arguments to pass to the prompt. Defaults to None.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
mcp.types.GetPromptResult: The complete response object from the protocol,
|
|
298
|
+
containing the prompt messages and any additional metadata.
|
|
299
|
+
|
|
300
|
+
Raises:
|
|
301
|
+
RuntimeError: If called while the client is not connected.
|
|
302
|
+
"""
|
|
303
|
+
result = await self.get_prompt_mcp(name=name, arguments=arguments)
|
|
304
|
+
return result
|
|
305
|
+
|
|
306
|
+
# --- Completion ---
|
|
307
|
+
|
|
308
|
+
async def complete_mcp(
|
|
309
|
+
self,
|
|
310
|
+
ref: mcp.types.ResourceReference | mcp.types.PromptReference,
|
|
311
|
+
argument: dict[str, str],
|
|
312
|
+
) -> mcp.types.CompleteResult:
|
|
313
|
+
"""Send a completion request and return the complete MCP protocol result.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
ref (mcp.types.ResourceReference | mcp.types.PromptReference): The reference to complete.
|
|
317
|
+
argument (dict[str, str]): Arguments to pass to the completion request.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
mcp.types.CompleteResult: The complete response object from the protocol,
|
|
321
|
+
containing the completion and any additional metadata.
|
|
322
|
+
|
|
323
|
+
Raises:
|
|
324
|
+
RuntimeError: If called while the client is not connected.
|
|
325
|
+
"""
|
|
326
|
+
result = await self.session.complete(ref=ref, argument=argument)
|
|
327
|
+
return result
|
|
173
328
|
|
|
174
329
|
async def complete(
|
|
175
330
|
self,
|
|
176
331
|
ref: mcp.types.ResourceReference | mcp.types.PromptReference,
|
|
177
332
|
argument: dict[str, str],
|
|
178
333
|
) -> mcp.types.Completion:
|
|
179
|
-
"""Send a completion request.
|
|
180
|
-
|
|
334
|
+
"""Send a completion request to the server.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
ref (mcp.types.ResourceReference | mcp.types.PromptReference): The reference to complete.
|
|
338
|
+
argument (dict[str, str]): Arguments to pass to the completion request.
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
mcp.types.Completion: The completion object.
|
|
342
|
+
|
|
343
|
+
Raises:
|
|
344
|
+
RuntimeError: If called while the client is not connected.
|
|
345
|
+
"""
|
|
346
|
+
result = await self.complete_mcp(ref=ref, argument=argument)
|
|
181
347
|
return result.completion
|
|
182
348
|
|
|
183
|
-
|
|
184
|
-
|
|
349
|
+
# --- Tools ---
|
|
350
|
+
|
|
351
|
+
async def list_tools_mcp(self) -> mcp.types.ListToolsResult:
|
|
352
|
+
"""Send a tools/list request and return the complete MCP protocol result.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
mcp.types.ListToolsResult: The complete response object from the protocol,
|
|
356
|
+
containing the list of tools and any additional metadata.
|
|
357
|
+
|
|
358
|
+
Raises:
|
|
359
|
+
RuntimeError: If called while the client is not connected.
|
|
360
|
+
"""
|
|
185
361
|
result = await self.session.list_tools()
|
|
362
|
+
return result
|
|
363
|
+
|
|
364
|
+
async def list_tools(self) -> list[mcp.types.Tool]:
|
|
365
|
+
"""Retrieve a list of tools available on the server.
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
list[mcp.types.Tool]: A list of Tool objects.
|
|
369
|
+
|
|
370
|
+
Raises:
|
|
371
|
+
RuntimeError: If called while the client is not connected.
|
|
372
|
+
"""
|
|
373
|
+
result = await self.list_tools_mcp()
|
|
186
374
|
return result.tools
|
|
187
375
|
|
|
188
|
-
|
|
376
|
+
# --- Call Tool ---
|
|
377
|
+
|
|
378
|
+
async def call_tool_mcp(
|
|
379
|
+
self, name: str, arguments: dict[str, Any]
|
|
380
|
+
) -> mcp.types.CallToolResult:
|
|
381
|
+
"""Send a tools/call request and return the complete MCP protocol result.
|
|
382
|
+
|
|
383
|
+
This method returns the raw CallToolResult object, which includes an isError flag
|
|
384
|
+
and other metadata. It does not raise an exception if the tool call results in an error.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
name (str): The name of the tool to call.
|
|
388
|
+
arguments (dict[str, Any]): Arguments to pass to the tool.
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
mcp.types.CallToolResult: The complete response object from the protocol,
|
|
392
|
+
containing the tool result and any additional metadata.
|
|
393
|
+
|
|
394
|
+
Raises:
|
|
395
|
+
RuntimeError: If called while the client is not connected.
|
|
396
|
+
"""
|
|
397
|
+
result = await self.session.call_tool(name=name, arguments=arguments)
|
|
398
|
+
return result
|
|
399
|
+
|
|
189
400
|
async def call_tool(
|
|
190
401
|
self,
|
|
191
402
|
name: str,
|
|
192
403
|
arguments: dict[str, Any] | None = None,
|
|
193
|
-
_return_raw_result: Literal[False] = False,
|
|
194
404
|
) -> list[
|
|
195
405
|
mcp.types.TextContent | mcp.types.ImageContent | mcp.types.EmbeddedResource
|
|
196
|
-
]:
|
|
406
|
+
]:
|
|
407
|
+
"""Call a tool on the server.
|
|
197
408
|
|
|
198
|
-
|
|
199
|
-
async def call_tool(
|
|
200
|
-
self,
|
|
201
|
-
name: str,
|
|
202
|
-
arguments: dict[str, Any] | None = None,
|
|
203
|
-
_return_raw_result: Literal[True] = True,
|
|
204
|
-
) -> mcp.types.CallToolResult: ...
|
|
409
|
+
Unlike call_tool_mcp, this method raises a ClientError if the tool call results in an error.
|
|
205
410
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
"""
|
|
218
|
-
result = await self.
|
|
219
|
-
if
|
|
220
|
-
return result
|
|
221
|
-
elif result.isError:
|
|
411
|
+
Args:
|
|
412
|
+
name (str): The name of the tool to call.
|
|
413
|
+
arguments (dict[str, Any] | None, optional): Arguments to pass to the tool. Defaults to None.
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
list[mcp.types.TextContent | mcp.types.ImageContent | mcp.types.EmbeddedResource]:
|
|
417
|
+
The content returned by the tool.
|
|
418
|
+
|
|
419
|
+
Raises:
|
|
420
|
+
ClientError: If the tool call results in an error.
|
|
421
|
+
RuntimeError: If called while the client is not connected.
|
|
422
|
+
"""
|
|
423
|
+
result = await self.call_tool_mcp(name=name, arguments=arguments or {})
|
|
424
|
+
if result.isError:
|
|
222
425
|
msg = cast(mcp.types.TextContent, result.content[0]).text
|
|
223
426
|
raise ClientError(msg)
|
|
224
427
|
return result.content
|
|
@@ -123,9 +123,7 @@ class BulkToolCaller(MCPMixin):
|
|
|
123
123
|
"""
|
|
124
124
|
|
|
125
125
|
async with Client(self.connection) as client:
|
|
126
|
-
result = await client.
|
|
127
|
-
name=tool, arguments=arguments, _return_raw_result=True
|
|
128
|
-
)
|
|
126
|
+
result = await client.call_tool_mcp(name=tool, arguments=arguments)
|
|
129
127
|
|
|
130
128
|
return CallToolRequestResult(
|
|
131
129
|
tool=tool,
|
fastmcp/prompts/__init__.py
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
from .prompt import Prompt,
|
|
1
|
+
from .prompt import Prompt, PromptMessage, Message
|
|
2
2
|
from .prompt_manager import PromptManager
|
|
3
3
|
|
|
4
|
-
__all__ = [
|
|
4
|
+
__all__ = [
|
|
5
|
+
"Prompt",
|
|
6
|
+
"PromptManager",
|
|
7
|
+
"PromptMessage",
|
|
8
|
+
"Message",
|
|
9
|
+
]
|
fastmcp/prompts/prompt.py
CHANGED
|
@@ -3,17 +3,21 @@
|
|
|
3
3
|
from __future__ import annotations as _annotations
|
|
4
4
|
|
|
5
5
|
import inspect
|
|
6
|
-
import json
|
|
7
6
|
from collections.abc import Awaitable, Callable, Sequence
|
|
8
|
-
from typing import TYPE_CHECKING, Annotated, Any
|
|
7
|
+
from typing import TYPE_CHECKING, Annotated, Any
|
|
9
8
|
|
|
10
9
|
import pydantic_core
|
|
11
|
-
from mcp.types import EmbeddedResource, ImageContent, TextContent
|
|
10
|
+
from mcp.types import EmbeddedResource, ImageContent, PromptMessage, Role, TextContent
|
|
12
11
|
from mcp.types import Prompt as MCPPrompt
|
|
13
12
|
from mcp.types import PromptArgument as MCPPromptArgument
|
|
14
13
|
from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, validate_call
|
|
15
14
|
|
|
16
|
-
from fastmcp.utilities.
|
|
15
|
+
from fastmcp.utilities.json_schema import prune_params
|
|
16
|
+
from fastmcp.utilities.types import (
|
|
17
|
+
_convert_set_defaults,
|
|
18
|
+
find_kwarg_by_type,
|
|
19
|
+
get_cached_typeadapter,
|
|
20
|
+
)
|
|
17
21
|
|
|
18
22
|
if TYPE_CHECKING:
|
|
19
23
|
from mcp.server.session import ServerSessionT
|
|
@@ -24,32 +28,24 @@ if TYPE_CHECKING:
|
|
|
24
28
|
CONTENT_TYPES = TextContent | ImageContent | EmbeddedResource
|
|
25
29
|
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
content:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
super().__init__(content=content, **kwargs)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def UserMessage(content: str | CONTENT_TYPES, **kwargs: Any) -> Message:
|
|
40
|
-
"""A message from the user."""
|
|
41
|
-
return Message(content=content, role="user", **kwargs)
|
|
31
|
+
def Message(
|
|
32
|
+
content: str | CONTENT_TYPES, role: Role | None = None, **kwargs: Any
|
|
33
|
+
) -> PromptMessage:
|
|
34
|
+
"""A user-friendly constructor for PromptMessage."""
|
|
35
|
+
if isinstance(content, str):
|
|
36
|
+
content = TextContent(type="text", text=content)
|
|
37
|
+
if role is None:
|
|
38
|
+
role = "user"
|
|
39
|
+
return PromptMessage(content=content, role=role, **kwargs)
|
|
42
40
|
|
|
43
41
|
|
|
44
|
-
|
|
45
|
-
"""A message from the assistant."""
|
|
46
|
-
return Message(content=content, role="assistant", **kwargs)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
message_validator = TypeAdapter[Message](Message)
|
|
42
|
+
message_validator = TypeAdapter[PromptMessage](PromptMessage)
|
|
50
43
|
|
|
51
44
|
SyncPromptResult = (
|
|
52
|
-
str
|
|
45
|
+
str
|
|
46
|
+
| PromptMessage
|
|
47
|
+
| dict[str, Any]
|
|
48
|
+
| Sequence[str | PromptMessage | dict[str, Any]]
|
|
53
49
|
)
|
|
54
50
|
PromptResult = SyncPromptResult | Awaitable[SyncPromptResult]
|
|
55
51
|
|
|
@@ -107,35 +103,32 @@ class Prompt(BaseModel):
|
|
|
107
103
|
|
|
108
104
|
if func_name == "<lambda>":
|
|
109
105
|
raise ValueError("You must provide a name for lambda functions")
|
|
106
|
+
# Reject functions with *args or **kwargs
|
|
107
|
+
sig = inspect.signature(fn)
|
|
108
|
+
for param in sig.parameters.values():
|
|
109
|
+
if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
|
110
|
+
raise ValueError("Functions with *args are not supported as prompts")
|
|
111
|
+
if param.kind == inspect.Parameter.VAR_KEYWORD:
|
|
112
|
+
raise ValueError("Functions with **kwargs are not supported as prompts")
|
|
113
|
+
|
|
114
|
+
type_adapter = get_cached_typeadapter(fn)
|
|
115
|
+
parameters = type_adapter.json_schema()
|
|
110
116
|
|
|
111
117
|
# Auto-detect context parameter if not provided
|
|
112
118
|
if context_kwarg is None:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
122
|
-
# Get schema from TypeAdapter - will fail if function isn't properly typed
|
|
123
|
-
parameters = TypeAdapter(fn).json_schema()
|
|
119
|
+
context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
|
|
120
|
+
if context_kwarg:
|
|
121
|
+
parameters = prune_params(parameters, params=[context_kwarg])
|
|
124
122
|
|
|
125
123
|
# Convert parameters to PromptArguments
|
|
126
124
|
arguments: list[PromptArgument] = []
|
|
127
125
|
if "properties" in parameters:
|
|
128
126
|
for param_name, param in parameters["properties"].items():
|
|
129
|
-
# Skip context parameter
|
|
130
|
-
if param_name == context_kwarg:
|
|
131
|
-
continue
|
|
132
|
-
|
|
133
|
-
required = param_name in parameters.get("required", [])
|
|
134
127
|
arguments.append(
|
|
135
128
|
PromptArgument(
|
|
136
129
|
name=param_name,
|
|
137
130
|
description=param.get("description"),
|
|
138
|
-
required=required,
|
|
131
|
+
required=param_name in parameters.get("required", []),
|
|
139
132
|
)
|
|
140
133
|
)
|
|
141
134
|
|
|
@@ -155,7 +148,7 @@ class Prompt(BaseModel):
|
|
|
155
148
|
self,
|
|
156
149
|
arguments: dict[str, Any] | None = None,
|
|
157
150
|
context: Context[ServerSessionT, LifespanContextT] | None = None,
|
|
158
|
-
) -> list[
|
|
151
|
+
) -> list[PromptMessage]:
|
|
159
152
|
"""Render the prompt with arguments."""
|
|
160
153
|
# Validate required arguments
|
|
161
154
|
if self.arguments:
|
|
@@ -181,19 +174,28 @@ class Prompt(BaseModel):
|
|
|
181
174
|
result = [result]
|
|
182
175
|
|
|
183
176
|
# Convert result to messages
|
|
184
|
-
messages: list[
|
|
185
|
-
for msg in result:
|
|
177
|
+
messages: list[PromptMessage] = []
|
|
178
|
+
for msg in result:
|
|
186
179
|
try:
|
|
187
|
-
if isinstance(msg,
|
|
180
|
+
if isinstance(msg, PromptMessage):
|
|
188
181
|
messages.append(msg)
|
|
189
|
-
elif isinstance(msg, dict):
|
|
190
|
-
messages.append(message_validator.validate_python(msg))
|
|
191
182
|
elif isinstance(msg, str):
|
|
192
|
-
|
|
193
|
-
|
|
183
|
+
messages.append(
|
|
184
|
+
PromptMessage(
|
|
185
|
+
role="user",
|
|
186
|
+
content=TextContent(type="text", text=msg),
|
|
187
|
+
)
|
|
188
|
+
)
|
|
194
189
|
else:
|
|
195
|
-
content =
|
|
196
|
-
|
|
190
|
+
content = pydantic_core.to_json(
|
|
191
|
+
msg, fallback=str, indent=2
|
|
192
|
+
).decode()
|
|
193
|
+
messages.append(
|
|
194
|
+
PromptMessage(
|
|
195
|
+
role="user",
|
|
196
|
+
content=TextContent(type="text", text=content),
|
|
197
|
+
)
|
|
198
|
+
)
|
|
197
199
|
except Exception:
|
|
198
200
|
raise ValueError(
|
|
199
201
|
f"Could not convert prompt result to message: {msg}"
|
|
@@ -5,8 +5,10 @@ from __future__ import annotations as _annotations
|
|
|
5
5
|
from collections.abc import Awaitable, Callable
|
|
6
6
|
from typing import TYPE_CHECKING, Any
|
|
7
7
|
|
|
8
|
+
from mcp import GetPromptResult
|
|
9
|
+
|
|
8
10
|
from fastmcp.exceptions import NotFoundError
|
|
9
|
-
from fastmcp.prompts.prompt import
|
|
11
|
+
from fastmcp.prompts.prompt import Prompt, PromptResult
|
|
10
12
|
from fastmcp.settings import DuplicateBehavior
|
|
11
13
|
from fastmcp.utilities.logging import get_logger
|
|
12
14
|
|
|
@@ -81,13 +83,18 @@ class PromptManager:
|
|
|
81
83
|
name: str,
|
|
82
84
|
arguments: dict[str, Any] | None = None,
|
|
83
85
|
context: Context[ServerSessionT, LifespanContextT] | None = None,
|
|
84
|
-
) ->
|
|
86
|
+
) -> GetPromptResult:
|
|
85
87
|
"""Render a prompt by name with arguments."""
|
|
86
88
|
prompt = self.get_prompt(name)
|
|
87
89
|
if not prompt:
|
|
88
90
|
raise NotFoundError(f"Unknown prompt: {name}")
|
|
89
91
|
|
|
90
|
-
|
|
92
|
+
messages = await prompt.render(arguments, context=context)
|
|
93
|
+
|
|
94
|
+
return GetPromptResult(
|
|
95
|
+
description=prompt.description,
|
|
96
|
+
messages=messages,
|
|
97
|
+
)
|
|
91
98
|
|
|
92
99
|
def has_prompt(self, key: str) -> bool:
|
|
93
100
|
"""Check if a prompt exists."""
|
fastmcp/resources/template.py
CHANGED
|
@@ -20,7 +20,10 @@ from pydantic import (
|
|
|
20
20
|
)
|
|
21
21
|
|
|
22
22
|
from fastmcp.resources.types import FunctionResource, Resource
|
|
23
|
-
from fastmcp.utilities.types import
|
|
23
|
+
from fastmcp.utilities.types import (
|
|
24
|
+
_convert_set_defaults,
|
|
25
|
+
find_kwarg_by_type,
|
|
26
|
+
)
|
|
24
27
|
|
|
25
28
|
if TYPE_CHECKING:
|
|
26
29
|
from mcp.server.session import ServerSessionT
|
|
@@ -106,23 +109,25 @@ class ResourceTemplate(BaseModel):
|
|
|
106
109
|
if func_name == "<lambda>":
|
|
107
110
|
raise ValueError("You must provide a name for lambda functions")
|
|
108
111
|
|
|
112
|
+
# Reject functions with *args
|
|
113
|
+
# (**kwargs is allowed because the URI will define the parameter names)
|
|
114
|
+
sig = inspect.signature(fn)
|
|
115
|
+
for param in sig.parameters.values():
|
|
116
|
+
if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
|
117
|
+
raise ValueError(
|
|
118
|
+
"Functions with *args are not supported as resource templates"
|
|
119
|
+
)
|
|
120
|
+
|
|
109
121
|
# Auto-detect context parameter if not provided
|
|
110
122
|
if context_kwarg is None:
|
|
111
|
-
|
|
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
|
|
123
|
+
context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
|
|
119
124
|
|
|
120
125
|
# Validate that URI params match function params
|
|
121
126
|
uri_params = set(re.findall(r"{(\w+)(?:\*)?}", uri_template))
|
|
122
127
|
if not uri_params:
|
|
123
128
|
raise ValueError("URI template must contain at least one parameter")
|
|
124
129
|
|
|
125
|
-
func_params = set(
|
|
130
|
+
func_params = set(sig.parameters.keys())
|
|
126
131
|
if context_kwarg:
|
|
127
132
|
func_params.discard(context_kwarg)
|
|
128
133
|
|
|
@@ -130,20 +135,26 @@ class ResourceTemplate(BaseModel):
|
|
|
130
135
|
required_params = {
|
|
131
136
|
p
|
|
132
137
|
for p in func_params
|
|
133
|
-
if
|
|
138
|
+
if sig.parameters[p].default is inspect.Parameter.empty
|
|
139
|
+
and sig.parameters[p].kind != inspect.Parameter.VAR_KEYWORD
|
|
140
|
+
and p != context_kwarg
|
|
134
141
|
}
|
|
135
|
-
if context_kwarg and context_kwarg in required_params:
|
|
136
|
-
required_params.discard(context_kwarg)
|
|
137
142
|
|
|
143
|
+
# Check if required parameters are a subset of the URI parameters
|
|
138
144
|
if not required_params.issubset(uri_params):
|
|
139
145
|
raise ValueError(
|
|
140
|
-
f"
|
|
146
|
+
f"Required function arguments {required_params} must be a subset of the URI parameters {uri_params}"
|
|
141
147
|
)
|
|
142
148
|
|
|
143
|
-
if
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
)
|
|
149
|
+
# Check if the URI parameters are a subset of the function parameters (skip if **kwargs present)
|
|
150
|
+
if not any(
|
|
151
|
+
param.kind == inspect.Parameter.VAR_KEYWORD
|
|
152
|
+
for param in sig.parameters.values()
|
|
153
|
+
):
|
|
154
|
+
if not uri_params.issubset(func_params):
|
|
155
|
+
raise ValueError(
|
|
156
|
+
f"URI parameters {uri_params} must be a subset of the function arguments: {func_params}"
|
|
157
|
+
)
|
|
147
158
|
|
|
148
159
|
# Get schema from TypeAdapter - will fail if function isn't properly typed
|
|
149
160
|
parameters = TypeAdapter(fn).json_schema()
|