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 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, Literal, cast, overload
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
- async def list_resources(self) -> list[mcp.types.Resource]:
132
- """Send a resources/list request."""
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 list_resource_templates(self) -> list[mcp.types.ResourceTemplate]:
137
- """Send a resources/listResourceTemplates request."""
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
- """Send a resources/read request."""
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.session.read_resource(uri)
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
- async def list_prompts(self) -> list[mcp.types.Prompt]:
163
- """Send a prompts/list request."""
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
- ) -> list[mcp.types.PromptMessage]:
170
- """Send a prompts/get request."""
171
- result = await self.session.get_prompt(name, arguments)
172
- return result.messages
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
- result = await self.session.complete(ref, argument)
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
- async def list_tools(self) -> list[mcp.types.Tool]:
184
- """Send a tools/list request."""
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
- @overload
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
- @overload
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
- async def call_tool(
207
- self,
208
- name: str,
209
- arguments: dict[str, Any] | None = None,
210
- _return_raw_result: bool = False,
211
- ) -> (
212
- list[
213
- mcp.types.TextContent | mcp.types.ImageContent | mcp.types.EmbeddedResource
214
- ]
215
- | mcp.types.CallToolResult
216
- ):
217
- """Send a tools/call request."""
218
- result = await self.session.call_tool(name, arguments)
219
- if _return_raw_result:
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.call_tool(
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,
@@ -1,4 +1,9 @@
1
- from .prompt import Prompt, Message, UserMessage, AssistantMessage
1
+ from .prompt import Prompt, PromptMessage, Message
2
2
  from .prompt_manager import PromptManager
3
3
 
4
- __all__ = ["Prompt", "PromptManager", "Message", "UserMessage", "AssistantMessage"]
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, Literal
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.types import _convert_set_defaults
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
- class Message(BaseModel):
28
- """Base class for all prompt messages."""
29
-
30
- role: Literal["user", "assistant"]
31
- content: CONTENT_TYPES
32
-
33
- def __init__(self, content: str | CONTENT_TYPES, **kwargs: Any):
34
- if isinstance(content, str):
35
- content = TextContent(type="text", text=content)
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
- def AssistantMessage(content: str | CONTENT_TYPES, **kwargs: Any) -> Message:
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 | Message | dict[str, Any] | Sequence[str | Message | dict[str, Any]]
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
- 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
-
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[Message]:
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[Message] = []
185
- for msg in result: # type: ignore[reportUnknownVariableType]
177
+ messages: list[PromptMessage] = []
178
+ for msg in result:
186
179
  try:
187
- if isinstance(msg, Message):
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
- content = TextContent(type="text", text=msg)
193
- messages.append(Message(role="user", content=content))
183
+ messages.append(
184
+ PromptMessage(
185
+ role="user",
186
+ content=TextContent(type="text", text=msg),
187
+ )
188
+ )
194
189
  else:
195
- content = json.dumps(pydantic_core.to_jsonable_python(msg))
196
- messages.append(Message(role="user", content=content))
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 Message, Prompt, PromptResult
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
- ) -> list[Message]:
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
- return await prompt.render(arguments, context=context)
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."""
@@ -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 _convert_set_defaults
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
- 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
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(inspect.signature(fn).parameters.keys())
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 inspect.signature(fn).parameters[p].default is inspect.Parameter.empty
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"URI parameters {uri_params} must be a subset of the required function arguments: {required_params}"
146
+ f"Required function arguments {required_params} must be a subset of the URI parameters {uri_params}"
141
147
  )
142
148
 
143
- if not uri_params.issubset(func_params):
144
- raise ValueError(
145
- f"URI parameters {uri_params} must be a subset of the function arguments: {func_params}"
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()