fastmcp 2.2.6__py3-none-any.whl → 2.2.7__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,190 @@ 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
289
  ) -> list[mcp.types.PromptMessage]:
170
- """Send a prompts/get request."""
171
- result = await self.session.get_prompt(name, arguments)
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
+ list[mcp.types.PromptMessage]: A list of prompt messages.
298
+
299
+ Raises:
300
+ RuntimeError: If called while the client is not connected.
301
+ """
302
+ result = await self.get_prompt_mcp(name=name, arguments=arguments)
172
303
  return result.messages
173
304
 
305
+ # --- Completion ---
306
+
307
+ async def complete_mcp(
308
+ self,
309
+ ref: mcp.types.ResourceReference | mcp.types.PromptReference,
310
+ argument: dict[str, str],
311
+ ) -> mcp.types.CompleteResult:
312
+ """Send a completion request and return the complete MCP protocol result.
313
+
314
+ Args:
315
+ ref (mcp.types.ResourceReference | mcp.types.PromptReference): The reference to complete.
316
+ argument (dict[str, str]): Arguments to pass to the completion request.
317
+
318
+ Returns:
319
+ mcp.types.CompleteResult: The complete response object from the protocol,
320
+ containing the completion and any additional metadata.
321
+
322
+ Raises:
323
+ RuntimeError: If called while the client is not connected.
324
+ """
325
+ result = await self.session.complete(ref=ref, argument=argument)
326
+ return result
327
+
174
328
  async def complete(
175
329
  self,
176
330
  ref: mcp.types.ResourceReference | mcp.types.PromptReference,
177
331
  argument: dict[str, str],
178
332
  ) -> mcp.types.Completion:
179
- """Send a completion request."""
180
- result = await self.session.complete(ref, argument)
333
+ """Send a completion request to the server.
334
+
335
+ Args:
336
+ ref (mcp.types.ResourceReference | mcp.types.PromptReference): The reference to complete.
337
+ argument (dict[str, str]): Arguments to pass to the completion request.
338
+
339
+ Returns:
340
+ mcp.types.Completion: The completion object.
341
+
342
+ Raises:
343
+ RuntimeError: If called while the client is not connected.
344
+ """
345
+ result = await self.complete_mcp(ref=ref, argument=argument)
181
346
  return result.completion
182
347
 
183
- async def list_tools(self) -> list[mcp.types.Tool]:
184
- """Send a tools/list request."""
348
+ # --- Tools ---
349
+
350
+ async def list_tools_mcp(self) -> mcp.types.ListToolsResult:
351
+ """Send a tools/list request and return the complete MCP protocol result.
352
+
353
+ Returns:
354
+ mcp.types.ListToolsResult: The complete response object from the protocol,
355
+ containing the list of tools and any additional metadata.
356
+
357
+ Raises:
358
+ RuntimeError: If called while the client is not connected.
359
+ """
185
360
  result = await self.session.list_tools()
361
+ return result
362
+
363
+ async def list_tools(self) -> list[mcp.types.Tool]:
364
+ """Retrieve a list of tools available on the server.
365
+
366
+ Returns:
367
+ list[mcp.types.Tool]: A list of Tool objects.
368
+
369
+ Raises:
370
+ RuntimeError: If called while the client is not connected.
371
+ """
372
+ result = await self.list_tools_mcp()
186
373
  return result.tools
187
374
 
188
- @overload
375
+ # --- Call Tool ---
376
+
377
+ async def call_tool_mcp(
378
+ self, name: str, arguments: dict[str, Any]
379
+ ) -> mcp.types.CallToolResult:
380
+ """Send a tools/call request and return the complete MCP protocol result.
381
+
382
+ This method returns the raw CallToolResult object, which includes an isError flag
383
+ and other metadata. It does not raise an exception if the tool call results in an error.
384
+
385
+ Args:
386
+ name (str): The name of the tool to call.
387
+ arguments (dict[str, Any]): Arguments to pass to the tool.
388
+
389
+ Returns:
390
+ mcp.types.CallToolResult: The complete response object from the protocol,
391
+ containing the tool result and any additional metadata.
392
+
393
+ Raises:
394
+ RuntimeError: If called while the client is not connected.
395
+ """
396
+ result = await self.session.call_tool(name=name, arguments=arguments)
397
+ return result
398
+
189
399
  async def call_tool(
190
400
  self,
191
401
  name: str,
192
402
  arguments: dict[str, Any] | None = None,
193
- _return_raw_result: Literal[False] = False,
194
403
  ) -> list[
195
404
  mcp.types.TextContent | mcp.types.ImageContent | mcp.types.EmbeddedResource
196
- ]: ...
405
+ ]:
406
+ """Call a tool on the server.
197
407
 
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: ...
408
+ Unlike call_tool_mcp, this method raises a ClientError if the tool call results in an error.
205
409
 
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:
410
+ Args:
411
+ name (str): The name of the tool to call.
412
+ arguments (dict[str, Any] | None, optional): Arguments to pass to the tool. Defaults to None.
413
+
414
+ Returns:
415
+ list[mcp.types.TextContent | mcp.types.ImageContent | mcp.types.EmbeddedResource]:
416
+ The content returned by the tool.
417
+
418
+ Raises:
419
+ ClientError: If the tool call results in an error.
420
+ RuntimeError: If called while the client is not connected.
421
+ """
422
+ result = await self.call_tool_mcp(name=name, arguments=arguments or {})
423
+ if result.isError:
222
424
  msg = cast(mcp.types.TextContent, result.content[0]).text
223
425
  raise ClientError(msg)
224
426
  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,
fastmcp/prompts/prompt.py CHANGED
@@ -3,7 +3,6 @@
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
7
  from typing import TYPE_CHECKING, Annotated, Any, Literal
9
8
 
@@ -13,7 +12,10 @@ 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.types import (
16
+ _convert_set_defaults,
17
+ is_class_member_of_type,
18
+ )
17
19
 
18
20
  if TYPE_CHECKING:
19
21
  from mcp.server.session import ServerSessionT
@@ -115,7 +117,7 @@ class Prompt(BaseModel):
115
117
  else:
116
118
  sig = inspect.signature(fn)
117
119
  for param_name, param in sig.parameters.items():
118
- if param.annotation is Context:
120
+ if is_class_member_of_type(param.annotation, Context):
119
121
  context_kwarg = param_name
120
122
  break
121
123
 
@@ -192,7 +194,9 @@ class Prompt(BaseModel):
192
194
  content = TextContent(type="text", text=msg)
193
195
  messages.append(Message(role="user", content=content))
194
196
  else:
195
- content = json.dumps(pydantic_core.to_jsonable_python(msg))
197
+ content = pydantic_core.to_json(
198
+ msg, fallback=str, indent=2
199
+ ).decode()
196
200
  messages.append(Message(role="user", content=content))
197
201
  except Exception:
198
202
  raise ValueError(
@@ -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
+ is_class_member_of_type,
26
+ )
24
27
 
25
28
  if TYPE_CHECKING:
26
29
  from mcp.server.session import ServerSessionT
@@ -113,7 +116,7 @@ class ResourceTemplate(BaseModel):
113
116
  else:
114
117
  sig = inspect.signature(fn)
115
118
  for param_name, param in sig.parameters.items():
116
- if param.annotation is Context:
119
+ if is_class_member_of_type(param.annotation, Context):
117
120
  context_kwarg = param_name
118
121
  break
119
122
 
@@ -97,15 +97,12 @@ class FunctionResource(Resource):
97
97
 
98
98
  if isinstance(result, Resource):
99
99
  return await result.read(context=context)
100
- if isinstance(result, bytes):
100
+ elif isinstance(result, bytes):
101
101
  return result
102
- if isinstance(result, str):
102
+ elif isinstance(result, str):
103
103
  return result
104
- try:
105
- return json.dumps(pydantic_core.to_jsonable_python(result))
106
- except (TypeError, pydantic_core.PydanticSerializationError):
107
- # If JSON serialization fails, try str()
108
- return str(result)
104
+ else:
105
+ return pydantic_core.to_json(result, fallback=str, indent=2).decode()
109
106
  except Exception as e:
110
107
  raise ValueError(f"Error reading resource {self.uri}: {e}")
111
108
 
fastmcp/server/context.py CHANGED
@@ -13,10 +13,12 @@ from mcp.types import (
13
13
  SamplingMessage,
14
14
  TextContent,
15
15
  )
16
- from pydantic import BaseModel
16
+ from pydantic import BaseModel, ConfigDict
17
17
  from pydantic.networks import AnyUrl
18
+ from starlette.requests import Request
18
19
 
19
20
  from fastmcp.server.server import FastMCP
21
+ from fastmcp.utilities.http import get_current_starlette_request
20
22
  from fastmcp.utilities.logging import get_logger
21
23
 
22
24
  logger = get_logger(__name__)
@@ -59,6 +61,8 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]):
59
61
  _request_context: RequestContext[ServerSessionT, LifespanContextT] | None
60
62
  _fastmcp: FastMCP | None
61
63
 
64
+ model_config = ConfigDict(arbitrary_types_allowed=True)
65
+
62
66
  def __init__(
63
67
  self,
64
68
  *,
@@ -222,3 +226,10 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]):
222
226
  )
223
227
 
224
228
  return result.content
229
+
230
+ def get_http_request(self) -> Request:
231
+ """Get the active starlette request."""
232
+ request = get_current_starlette_request()
233
+ if request is None:
234
+ raise ValueError("Request is not available outside a Starlette request")
235
+ return request
fastmcp/server/openapi.py CHANGED
@@ -5,12 +5,13 @@ from __future__ import annotations
5
5
  import enum
6
6
  import json
7
7
  import re
8
+ from collections.abc import Callable
8
9
  from dataclasses import dataclass
9
10
  from re import Pattern
10
11
  from typing import TYPE_CHECKING, Any, Literal
11
12
 
12
13
  import httpx
13
- from mcp.types import EmbeddedResource, ImageContent, TextContent
14
+ from mcp.types import EmbeddedResource, ImageContent, TextContent, ToolAnnotations
14
15
  from pydantic.networks import AnyUrl
15
16
 
16
17
  from fastmcp.resources import Resource, ResourceTemplate
@@ -126,6 +127,8 @@ class OpenAPITool(Tool):
126
127
  is_async: bool = True,
127
128
  tags: set[str] = set(),
128
129
  timeout: float | None = None,
130
+ annotations: ToolAnnotations | None = None,
131
+ serializer: Callable[[Any], str] | None = None,
129
132
  ):
130
133
  super().__init__(
131
134
  name=name,
@@ -136,6 +139,8 @@ class OpenAPITool(Tool):
136
139
  is_async=is_async,
137
140
  context_kwarg="context", # Default context keyword argument
138
141
  tags=tags,
142
+ annotations=annotations,
143
+ serializer=serializer,
139
144
  )
140
145
  self._client = client
141
146
  self._route = route
@@ -534,10 +539,12 @@ class FastMCPOpenAPI(FastMCP):
534
539
  or f"Executes {route.method} {route.path}"
535
540
  )
536
541
 
537
- # Format enhanced description
542
+ # Format enhanced description with parameters and request body
538
543
  enhanced_description = format_description_with_responses(
539
544
  base_description=base_description,
540
545
  responses=route.responses,
546
+ parameters=route.parameters,
547
+ request_body=route.request_body,
541
548
  )
542
549
 
543
550
  tool = OpenAPITool(
@@ -565,10 +572,12 @@ class FastMCPOpenAPI(FastMCP):
565
572
  route.description or route.summary or f"Represents {route.path}"
566
573
  )
567
574
 
568
- # Format enhanced description
575
+ # Format enhanced description with parameters and request body
569
576
  enhanced_description = format_description_with_responses(
570
577
  base_description=base_description,
571
578
  responses=route.responses,
579
+ parameters=route.parameters,
580
+ request_body=route.request_body,
572
581
  )
573
582
 
574
583
  resource = OpenAPIResource(
@@ -600,16 +609,30 @@ class FastMCPOpenAPI(FastMCP):
600
609
  route.description or route.summary or f"Template for {route.path}"
601
610
  )
602
611
 
603
- # Format enhanced description
612
+ # Format enhanced description with parameters and request body
604
613
  enhanced_description = format_description_with_responses(
605
614
  base_description=base_description,
606
615
  responses=route.responses,
616
+ parameters=route.parameters,
617
+ request_body=route.request_body,
607
618
  )
608
619
 
609
620
  template_params_schema = {
610
621
  "type": "object",
611
622
  "properties": {
612
- p.name: p.schema_ for p in route.parameters if p.location == "path"
623
+ p.name: {
624
+ **(p.schema_.copy() if isinstance(p.schema_, dict) else {}),
625
+ **(
626
+ {"description": p.description}
627
+ if p.description
628
+ and not (
629
+ isinstance(p.schema_, dict) and "description" in p.schema_
630
+ )
631
+ else {}
632
+ ),
633
+ }
634
+ for p in route.parameters
635
+ if p.location == "path"
613
636
  },
614
637
  "required": [
615
638
  p.name for p in route.parameters if p.location == "path" and p.required
fastmcp/server/proxy.py CHANGED
@@ -65,8 +65,9 @@ class ProxyTool(Tool):
65
65
  # the client context manager will swallow any exceptions inside a TaskGroup
66
66
  # so we return the raw result and raise an exception ourselves
67
67
  async with self._client:
68
- result = await self._client.call_tool(
69
- self.name, arguments, _return_raw_result=True
68
+ result = await self._client.call_tool_mcp(
69
+ name=self.name,
70
+ arguments=arguments,
70
71
  )
71
72
  if result.isError:
72
73
  raise ValueError(cast(mcp.types.TextContent, result.content[0]).text)