Skip to content

Commit b711519

Browse files
authored
Merge pull request jlowin#301 from jlowin/client
Add low-level methods to client
2 parents b36c871 + 4adb61a commit b711519

File tree

6 files changed

+383
-48
lines changed

6 files changed

+383
-48
lines changed

docs/clients/client.mdx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,34 @@ The `Client` provides methods corresponding to standard MCP requests:
149149
* **`list_prompts()`**: Retrieves available prompt templates.
150150
* **`get_prompt(name: str, arguments: dict[str, Any] | None = None)`**: Retrieves a rendered prompt message list.
151151

152+
### Raw MCP Protocol Objects
153+
154+
The FastMCP client attempts to provide a "friendly" interface to the MCP protocol, but sometimes you may need access to the raw MCP protocol objects. Each of the main client methods that returns data has a corresponding `*_mcp` method that returns the raw MCP protocol objects directly.
155+
156+
```python
157+
# Standard method - returns just the list of tools
158+
tools = await client.list_tools()
159+
# tools -> list[mcp.types.Tool]
160+
161+
# Raw MCP method - returns the full protocol object
162+
result = await client.list_tools_mcp()
163+
# result -> mcp.types.ListToolsResult
164+
tools = result.tools
165+
```
166+
167+
Available raw MCP methods:
168+
169+
* **`list_tools_mcp()`**: Returns `mcp.types.ListToolsResult`
170+
* **`call_tool_mcp(name, arguments)`**: Returns `mcp.types.CallToolResult`
171+
* **`list_resources_mcp()`**: Returns `mcp.types.ListResourcesResult`
172+
* **`list_resource_templates_mcp()`**: Returns `mcp.types.ListResourceTemplatesResult`
173+
* **`read_resource_mcp(uri)`**: Returns `mcp.types.ReadResourceResult`
174+
* **`list_prompts_mcp()`**: Returns `mcp.types.ListPromptsResult`
175+
* **`get_prompt_mcp(name, arguments)`**: Returns `mcp.types.GetPromptResult`
176+
* **`complete_mcp(ref, argument)`**: Returns `mcp.types.CompleteResult`
177+
178+
These methods are especially useful for debugging or when you need to access metadata or fields that aren't exposed by the simplified methods.
179+
152180
### Advanced Features
153181

154182
MCP allows servers to interact with clients in order to provide additional capabilities. The `Client` constructor accepts additional configuration to handle these server requests.
@@ -268,5 +296,5 @@ async def safe_call_tool():
268296
Other errors, like connection failures, will raise standard Python exceptions (e.g., `ConnectionError`, `TimeoutError`).
269297

270298
<Tip>
271-
The client transport often has its own error-handling mechanisms, so you can not always trap errors like those raised by `call_tool` outside of the `async with` block. Instead, you can call `call_tool(..., _return_raw_result=True)` to get the raw `mcp.types.CallToolResult` object and handle errors yourself by checking its `isError` attribute.
299+
The client transport often has its own error-handling mechanisms, so you can not always trap errors like those raised by `call_tool` outside of the `async with` block. Instead, you can use `call_tool_mcp()` to get the raw `mcp.types.CallToolResult` object and handle errors yourself by checking its `isError` attribute.
272300
</Tip>

src/fastmcp/client/client.py

Lines changed: 243 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import datetime
22
from contextlib import AbstractAsyncContextManager
33
from pathlib import Path
4-
from typing import Any, Literal, cast, overload
4+
from typing import Any, cast
55

66
import mcp.types
77
from mcp import ClientSession
@@ -107,6 +107,7 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
107107
self._session = None
108108

109109
# --- MCP Client Methods ---
110+
110111
async def ping(self) -> None:
111112
"""Send a ping request."""
112113
await self.session.send_ping()
@@ -128,23 +129,100 @@ async def send_roots_list_changed(self) -> None:
128129
"""Send a roots/list_changed notification."""
129130
await self.session.send_roots_list_changed()
130131

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+
"""
133144
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()
134157
return result.resources
135158

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+
"""
138171
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()
139186
return result.resourceTemplates
140187

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+
141208
async def read_resource(
142209
self, uri: AnyUrl | str
143210
) -> 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+
"""
145223
if isinstance(uri, str):
146224
uri = AnyUrl(uri) # Ensure AnyUrl
147-
result = await self.session.read_resource(uri)
225+
result = await self.read_resource_mcp(uri)
148226
return result.contents
149227

150228
# async def subscribe_resource(self, uri: AnyUrl | str) -> None:
@@ -159,66 +237,190 @@ async def read_resource(
159237
# uri = AnyUrl(uri)
160238
# await self.session.unsubscribe_resource(uri)
161239

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+
"""
164252
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()
165265
return result.prompts
166266

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+
167287
async def get_prompt(
168288
self, name: str, arguments: dict[str, str] | None = None
169289
) -> 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)
172303
return result.messages
173304

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+
174328
async def complete(
175329
self,
176330
ref: mcp.types.ResourceReference | mcp.types.PromptReference,
177331
argument: dict[str, str],
178332
) -> 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)
181346
return result.completion
182347

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+
"""
185360
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()
186373
return result.tools
187374

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+
189399
async def call_tool(
190400
self,
191401
name: str,
192402
arguments: dict[str, Any] | None = None,
193-
_return_raw_result: Literal[False] = False,
194403
) -> list[
195404
mcp.types.TextContent | mcp.types.ImageContent | mcp.types.EmbeddedResource
196-
]: ...
405+
]:
406+
"""Call a tool on the server.
197407
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.
205409
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:
222424
msg = cast(mcp.types.TextContent, result.content[0]).text
223425
raise ClientError(msg)
224426
return result.content

0 commit comments

Comments
 (0)