From 204701cb88bb6cdbae581ee724ef21ed1004ccab Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Sat, 12 Jul 2025 12:06:26 +0400 Subject: [PATCH 01/10] Add prompts caching + working examples --- examples/mcp/caching/README.md | 13 ++++++ examples/mcp/caching/main.py | 74 ++++++++++++++++++++++++++++++ examples/mcp/caching/server.py | 37 +++++++++++++++ src/agents/mcp/server.py | 83 ++++++++++++++++++++++++++++------ 4 files changed, 192 insertions(+), 15 deletions(-) create mode 100644 examples/mcp/caching/README.md create mode 100644 examples/mcp/caching/main.py create mode 100644 examples/mcp/caching/server.py diff --git a/examples/mcp/caching/README.md b/examples/mcp/caching/README.md new file mode 100644 index 000000000..667cc5714 --- /dev/null +++ b/examples/mcp/caching/README.md @@ -0,0 +1,13 @@ +# Caching Example + +This example show how to integrate tools and prompts caching using a Streamable HTTP server in [server.py](server.py). + +Run the example via: + +``` +uv run python examples/mcp/caching/main.py +``` + +## Details + +The example uses the `MCPServerStreamableHttp` class from `agents.mcp`. The server runs in a sub-process at `https://localhost:8000/mcp`. diff --git a/examples/mcp/caching/main.py b/examples/mcp/caching/main.py new file mode 100644 index 000000000..c4ec2c1f0 --- /dev/null +++ b/examples/mcp/caching/main.py @@ -0,0 +1,74 @@ +import asyncio +import os +import shutil +import subprocess +import time +from typing import Any + +from agents import gen_trace_id, trace +from agents.mcp import MCPServerStreamableHttp + + +async def run(mcp_server: MCPServerStreamableHttp): + print(f"Cached tools before invoking tool_list") + print(mcp_server._tools_list) + await mcp_server.list_tools() + print(f"Cached tools names after invoking list_tools") + cached_tools_list = mcp_server._tools_list + for tool in cached_tools_list: + print(f"name: {tool.name}") + + print(f"Cached prompts before invoking list_prompts") + print(mcp_server._prompts_list) + await mcp_server.list_prompts() + print(f"\nCached prompts after invoking list_prompts") + cached_prompts_list = mcp_server._prompts_list + for prompt in cached_prompts_list.prompts: + print(f"name: {prompt.name}") + +async def main(): + async with MCPServerStreamableHttp( + name="Streamable HTTP Python Server", + cache_tools_list=True, + cache_prompts_list=True, + params={ + "url": "http://localhost:8000/mcp", + }, + ) as server: + trace_id = gen_trace_id() + with trace(workflow_name="Caching Example", trace_id=trace_id): + print(f"View trace: https://platform.openai.com/traces/trace?trace_id={trace_id}\n") + await run(server) + + +if __name__ == "__main__": + # Let's make sure the user has uv installed + if not shutil.which("uv"): + raise RuntimeError( + "uv is not installed. Please install it: https://docs.astral.sh/uv/getting-started/installation/" + ) + + # We'll run the Streamable HTTP server in a subprocess. Usually this would be a remote server, but for this + # demo, we'll run it locally at http://localhost:8000/mcp + process: subprocess.Popen[Any] | None = None + try: + this_dir = os.path.dirname(os.path.abspath(__file__)) + server_file = os.path.join(this_dir, "server.py") + + print("Starting Streamable HTTP server at http://localhost:8000/mcp ...") + + # Run `uv run server.py` to start the Streamable HTTP server + process = subprocess.Popen(["uv", "run", server_file]) + # Give it 3 seconds to start + time.sleep(3) + + print("Streamable HTTP server started. Running example...\n\n") + except Exception as e: + print(f"Error starting Streamable HTTP server: {e}") + exit(1) + + try: + asyncio.run(main()) + finally: + if process: + process.terminate() diff --git a/examples/mcp/caching/server.py b/examples/mcp/caching/server.py new file mode 100644 index 000000000..0a031dc8d --- /dev/null +++ b/examples/mcp/caching/server.py @@ -0,0 +1,37 @@ +import random + +import requests +from mcp.server.fastmcp import FastMCP + +# Create server +mcp = FastMCP("Echo Server") + + +@mcp.tool() +def add(a: int, b: int) -> int: + """Add two numbers""" + print(f"[debug-server] add({a}, {b})") + return a + b + + +@mcp.tool() +def get_secret_word() -> str: + print("[debug-server] get_secret_word()") + return random.choice(["apple", "banana", "cherry"]) + + +@mcp.tool() +def get_current_weather(city: str) -> str: + print(f"[debug-server] get_current_weather({city})") + + endpoint = "https://wttr.in" + response = requests.get(f"{endpoint}/{city}") + return response.text + +@mcp.prompt() +def system_prompt() -> str: + return "Use the tools to answer the questions." + + +if __name__ == "__main__": + mcp.run(transport="streamable-http") diff --git a/src/agents/mcp/server.py b/src/agents/mcp/server.py index 91a9274fc..9c11235d1 100644 --- a/src/agents/mcp/server.py +++ b/src/agents/mcp/server.py @@ -84,6 +84,7 @@ class _MCPServerWithClientSession(MCPServer, abc.ABC): def __init__( self, cache_tools_list: bool, + cache_prompts_list: bool, client_session_timeout_seconds: float | None, tool_filter: ToolFilter = None, ): @@ -96,6 +97,13 @@ def __init__( server will not change its tools list, because it can drastically improve latency (by avoiding a round-trip to the server every time). + cache_prompts_list: Whether to cache the prompts list. If `True`, the prompts list will be + cached and only fetched from the server once. If `False`, the prompts list will be + fetched from the server on each call to `list_prompts()`. The cache can be invalidated + by calling `invalidate_prompts_cache()`. You should set this to `True` if you know the + server will not change its prompts list, because it can drastically improve latency + (by avoiding a round-trip to the server every time). + client_session_timeout_seconds: the read timeout passed to the MCP ClientSession. tool_filter: The tool filter to use for filtering tools. """ @@ -103,13 +111,16 @@ def __init__( self.exit_stack: AsyncExitStack = AsyncExitStack() self._cleanup_lock: asyncio.Lock = asyncio.Lock() self.cache_tools_list = cache_tools_list + self.cache_prompts_list = cache_prompts_list self.server_initialize_result: InitializeResult | None = None self.client_session_timeout_seconds = client_session_timeout_seconds - # The cache is always dirty at startup, so that we fetch tools at least once - self._cache_dirty = True + # The cache is always dirty at startup, so that we fetch tools and prompts at least once + self._cache_dirty_tools = True self._tools_list: list[MCPTool] | None = None + self._cache_dirty_prompts = True + self._prompts_list: ListPromptsResult | None = None self.tool_filter = tool_filter @@ -213,7 +224,11 @@ async def __aexit__(self, exc_type, exc_value, traceback): def invalidate_tools_cache(self): """Invalidate the tools cache.""" - self._cache_dirty = True + self._cache_dirty_tools = True + + def invalidate_prompts_cache(self): + """Invalidate the prompts cache.""" + self._cache_dirty_prompts = True async def connect(self): """Connect to the server.""" @@ -251,11 +266,11 @@ async def list_tools( raise UserError("Server not initialized. Make sure you call `connect()` first.") # Return from cache if caching is enabled, we have tools, and the cache is not dirty - if self.cache_tools_list and not self._cache_dirty and self._tools_list: + if self.cache_tools_list and not self._cache_dirty_tools and self._tools_list: tools = self._tools_list else: # Reset the cache dirty to False - self._cache_dirty = False + self._cache_dirty_tools = False # Fetch the tools from the server self._tools_list = (await self.session.list_tools()).tools tools = self._tools_list @@ -282,7 +297,16 @@ async def list_prompts( if not self.session: raise UserError("Server not initialized. Make sure you call `connect()` first.") - return await self.session.list_prompts() + if self.cache_prompts_list and not self._cache_dirty_prompts and self._prompts_list: + prompts = self._prompts_list + else: + # Reset the cache dirty to False + self._cache_dirty_prompts = False + # Fetch the prompts from the server + self._prompts_list = await self.session.list_prompts() + prompts = self._tools_list + + return prompts async def get_prompt( self, name: str, arguments: dict[str, Any] | None = None @@ -343,6 +367,7 @@ def __init__( self, params: MCPServerStdioParams, cache_tools_list: bool = False, + cache_prompts_list: bool = False, name: str | None = None, client_session_timeout_seconds: float | None = 5, tool_filter: ToolFilter = None, @@ -354,21 +379,31 @@ def __init__( start the server, the args to pass to the command, the environment variables to set for the server, the working directory to use when spawning the process, and the text encoding used when sending/receiving messages to the server. + cache_tools_list: Whether to cache the tools list. If `True`, the tools list will be cached and only fetched from the server once. If `False`, the tools list will be fetched from the server on each call to `list_tools()`. The cache can be invalidated by calling `invalidate_tools_cache()`. You should set this to `True` if you know the server will not change its tools list, because it can drastically improve latency (by avoiding a round-trip to the server every time). + + cache_prompts_list: Whether to cache the prompts list. If `True`, the prompts list will be + cached and only fetched from the server once. If `False`, the prompts list will be + fetched from the server on each call to `list_prompts()`. The cache can be invalidated + by calling `invalidate_prompts_cache()`. You should set this to `True` if you know the + server will not change its prompts list, because it can drastically improve latency + (by avoiding a round-trip to the server every time). + name: A readable name for the server. If not provided, we'll create one from the command. client_session_timeout_seconds: the read timeout passed to the MCP ClientSession. tool_filter: The tool filter to use for filtering tools. """ super().__init__( - cache_tools_list, - client_session_timeout_seconds, - tool_filter, + cache_tools_list=cache_tools_list, + cache_prompts_list=cache_prompts_list, + client_session_timeout_seconds=client_session_timeout_seconds, + tool_filter=tool_filter, ) self.params = StdioServerParameters( @@ -426,6 +461,7 @@ def __init__( self, params: MCPServerSseParams, cache_tools_list: bool = False, + cache_prompts_list: bool = False, name: str | None = None, client_session_timeout_seconds: float | None = 5, tool_filter: ToolFilter = None, @@ -444,6 +480,13 @@ def __init__( if you know the server will not change its tools list, because it can drastically improve latency (by avoiding a round-trip to the server every time). + cache_prompts_list: Whether to cache the prompts list. If `True`, the prompts list will be + cached and only fetched from the server once. If `False`, the prompts list will be + fetched from the server on each call to `list_prompts()`. The cache can be invalidated + by calling `invalidate_prompts_cache()`. You should set this to `True` if you know the + server will not change its prompts list, because it can drastically improve latency + (by avoiding a round-trip to the server every time). + name: A readable name for the server. If not provided, we'll create one from the URL. @@ -451,9 +494,10 @@ def __init__( tool_filter: The tool filter to use for filtering tools. """ super().__init__( - cache_tools_list, - client_session_timeout_seconds, - tool_filter, + cache_tools_list=cache_tools_list, + cache_prompts_list=cache_prompts_list, + client_session_timeout_seconds=client_session_timeout_seconds, + tool_filter=tool_filter, ) self.params = params @@ -511,6 +555,7 @@ def __init__( self, params: MCPServerStreamableHttpParams, cache_tools_list: bool = False, + cache_prompts_list: bool = False, name: str | None = None, client_session_timeout_seconds: float | None = 5, tool_filter: ToolFilter = None, @@ -530,6 +575,13 @@ def __init__( if you know the server will not change its tools list, because it can drastically improve latency (by avoiding a round-trip to the server every time). + cache_prompts_list: Whether to cache the prompts list. If `True`, the prompts list will be + cached and only fetched from the server once. If `False`, the prompts list will be + fetched from the server on each call to `list_prompts()`. The cache can be invalidated + by calling `invalidate_prompts_cache()`. You should set this to `True` if you know the + server will not change its prompts list, because it can drastically improve latency + (by avoiding a round-trip to the server every time). + name: A readable name for the server. If not provided, we'll create one from the URL. @@ -537,9 +589,10 @@ def __init__( tool_filter: The tool filter to use for filtering tools. """ super().__init__( - cache_tools_list, - client_session_timeout_seconds, - tool_filter, + cache_tools_list=cache_tools_list, + cache_prompts_list=cache_prompts_list, + client_session_timeout_seconds=client_session_timeout_seconds, + tool_filter=tool_filter, ) self.params = params From 90a349f6035bb29db1a1f5899222ebe7d61d3043 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Sat, 12 Jul 2025 12:09:10 +0400 Subject: [PATCH 02/10] Update docs --- docs/mcp.md | 4 ++-- examples/mcp/caching/main.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/mcp.md b/docs/mcp.md index eef61a047..d49791975 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -169,9 +169,9 @@ agent = Agent( ## Caching -Every time an Agent runs, it calls `list_tools()` on the MCP server. This can be a latency hit, especially if the server is a remote server. To automatically cache the list of tools, you can pass `cache_tools_list=True` to [`MCPServerStdio`][agents.mcp.server.MCPServerStdio], [`MCPServerSse`][agents.mcp.server.MCPServerSse], and [`MCPServerStreamableHttp`][agents.mcp.server.MCPServerStreamableHttp]. You should only do this if you're certain the tool list will not change. +Every time an Agent runs, it calls `list_tools()` and `list_prompts()` on the MCP server. This can be a latency hit, especially if the server is a remote server. To automatically cache the list of tools and prompts, you can pass `cache_tools_list=True` and `cache_prompts_list=True` to [`MCPServerStdio`][agents.mcp.server.MCPServerStdio], [`MCPServerSse`][agents.mcp.server.MCPServerSse], and [`MCPServerStreamableHttp`][agents.mcp.server.MCPServerStreamableHttp]. You should only do this if you're certain the tools and the prompts lists will not change. -If you want to invalidate the cache, you can call `invalidate_tools_cache()` on the servers. +If you want to invalidate the cache, you can call `invalidate_tools_cache()` and `invalidate_prompts_cache()` on the servers. ## End-to-end examples diff --git a/examples/mcp/caching/main.py b/examples/mcp/caching/main.py index c4ec2c1f0..c3e98ddc5 100644 --- a/examples/mcp/caching/main.py +++ b/examples/mcp/caching/main.py @@ -12,16 +12,18 @@ async def run(mcp_server: MCPServerStreamableHttp): print(f"Cached tools before invoking tool_list") print(mcp_server._tools_list) - await mcp_server.list_tools() + print(f"Cached tools names after invoking list_tools") + await mcp_server.list_tools() cached_tools_list = mcp_server._tools_list for tool in cached_tools_list: print(f"name: {tool.name}") print(f"Cached prompts before invoking list_prompts") print(mcp_server._prompts_list) - await mcp_server.list_prompts() + print(f"\nCached prompts after invoking list_prompts") + await mcp_server.list_prompts() cached_prompts_list = mcp_server._prompts_list for prompt in cached_prompts_list.prompts: print(f"name: {prompt.name}") From 7f68fab167150832b1f51bf5d3ae873dce6a511f Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Sat, 12 Jul 2025 12:23:34 +0400 Subject: [PATCH 03/10] Add unit test test_server_caching_prompts_works --- tests/mcp/test_caching.py | 53 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/tests/mcp/test_caching.py b/tests/mcp/test_caching.py index f31cdf951..80fbb1898 100644 --- a/tests/mcp/test_caching.py +++ b/tests/mcp/test_caching.py @@ -1,7 +1,7 @@ from unittest.mock import AsyncMock, patch import pytest -from mcp.types import ListToolsResult, Tool as MCPTool +from mcp.types import ListPromptsResult, ListToolsResult, Prompt, Tool as MCPTool from agents import Agent from agents.mcp import MCPServerStdio @@ -14,7 +14,7 @@ @patch("mcp.client.stdio.stdio_client", return_value=DummyStreamsContextManager()) @patch("mcp.client.session.ClientSession.initialize", new_callable=AsyncMock, return_value=None) @patch("mcp.client.session.ClientSession.list_tools") -async def test_server_caching_works( +async def test_server_caching_tools_works( mock_list_tools: AsyncMock, mock_initialize: AsyncMock, mock_stdio_client ): """Test that if we turn caching on, the list of tools is cached and not fetched from the server @@ -61,3 +61,52 @@ async def test_server_caching_works( # Without invalidating the cache, calling list_tools() again should return the cached value result_tools = await server.list_tools(run_context, agent) assert result_tools == tools + +@pytest.mark.asyncio +@patch("mcp.client.stdio.stdio_client", return_value=DummyStreamsContextManager()) +@patch("mcp.client.session.ClientSession.initialize", new_callable=AsyncMock, return_value=None) +@patch("mcp.client.session.ClientSession.list_tools") +async def test_server_caching_prompts_works( + mock_list_prompts: AsyncMock, mock_initialize: AsyncMock, mock_stdio_client +): + """Test that if we turn caching on, the list of prompts is cached and not fetched from the server + on each call to `list_prompts()`. + """ + server = MCPServerStdio( + params={ + "command": tee, + }, + cache_prompts_list=True, + ) + + prompts = [ + Prompt(name="prompt1"), + Prompt(name="prompt2"), + ] + + mock_list_prompts.return_value = ListPromptsResult(prompts=prompts) + + async with server: + + # Call list_prompts() multiple times + result_prompts = await server.list_prompts() + assert result_prompts == prompts + + assert mock_list_prompts.call_count == 1, "list_prompts() should have been called once" + + # Call list_prompts() again, should return the cached value + result_prompts = await server.list_prompts() + assert result_prompts == prompts + + assert mock_list_prompts.call_count == 1, "list_prompts() should not have been called again" + + # Invalidate the cache and call list_prompts() again + server.invalidate_prompts_cache() + result_prompts = await server.list_prompts() + assert result_prompts == prompts + + assert mock_list_prompts.call_count == 2, "list_prompts() should be called again" + + # Without invalidating the cache, calling list_prompts() again should return the cached value + result_prompts = await server.list_prompts() + assert result_prompts == prompts From 645aede3ca56888222aac89e0fab26c0c231f15c Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Sat, 12 Jul 2025 12:31:59 +0400 Subject: [PATCH 04/10] Fix lint --- examples/mcp/caching/main.py | 8 ++--- src/agents/mcp/server.py | 58 +++++++++++++++++++----------------- tests/mcp/test_caching.py | 13 ++++---- 3 files changed, 43 insertions(+), 36 deletions(-) diff --git a/examples/mcp/caching/main.py b/examples/mcp/caching/main.py index c3e98ddc5..724b0f972 100644 --- a/examples/mcp/caching/main.py +++ b/examples/mcp/caching/main.py @@ -10,19 +10,19 @@ async def run(mcp_server: MCPServerStreamableHttp): - print(f"Cached tools before invoking tool_list") + print("Cached tools before invoking tool_list") print(mcp_server._tools_list) - print(f"Cached tools names after invoking list_tools") + print("Cached tools names after invoking list_tools") await mcp_server.list_tools() cached_tools_list = mcp_server._tools_list for tool in cached_tools_list: print(f"name: {tool.name}") - print(f"Cached prompts before invoking list_prompts") + print("Cached prompts before invoking list_prompts") print(mcp_server._prompts_list) - print(f"\nCached prompts after invoking list_prompts") + print("\nCached prompts after invoking list_prompts") await mcp_server.list_prompts() cached_prompts_list = mcp_server._prompts_list for prompt in cached_prompts_list.prompts: diff --git a/src/agents/mcp/server.py b/src/agents/mcp/server.py index 9c11235d1..6a8434d64 100644 --- a/src/agents/mcp/server.py +++ b/src/agents/mcp/server.py @@ -91,18 +91,19 @@ def __init__( """ Args: cache_tools_list: Whether to cache the tools list. If `True`, the tools list will be - cached and only fetched from the server once. If `False`, the tools list will be - fetched from the server on each call to `list_tools()`. The cache can be invalidated - by calling `invalidate_tools_cache()`. You should set this to `True` if you know the - server will not change its tools list, because it can drastically improve latency - (by avoiding a round-trip to the server every time). - - cache_prompts_list: Whether to cache the prompts list. If `True`, the prompts list will be - cached and only fetched from the server once. If `False`, the prompts list will be - fetched from the server on each call to `list_prompts()`. The cache can be invalidated - by calling `invalidate_prompts_cache()`. You should set this to `True` if you know the - server will not change its prompts list, because it can drastically improve latency - (by avoiding a round-trip to the server every time). + cached and only fetched from the server once. If `False`, the tools list will be + fetched from the server on each call to `list_tools()`. The cache can be invalidated + by calling `invalidate_tools_cache()`. You should set this to `True` if you know the + server will not change its tools list, because it can drastically improve latency + (by avoiding a round-trip to the server every time). + + cache_prompts_list: Whether to cache the prompts list. If `True`, the prompts list + will be cached and only fetched from the server once. If `False`, the prompts + list will be fetched from the server on each call to `list_prompts()`. + The cache can be invalidated by calling `invalidate_prompts_cache()`. + You should set this to `True` if you know the server will not change + its prompts list, because it can drastically improve latency + (by avoiding a round-trip to the server every time). client_session_timeout_seconds: the read timeout passed to the MCP ClientSession. tool_filter: The tool filter to use for filtering tools. @@ -387,11 +388,12 @@ def __init__( if you know the server will not change its tools list, because it can drastically improve latency (by avoiding a round-trip to the server every time). - cache_prompts_list: Whether to cache the prompts list. If `True`, the prompts list will be - cached and only fetched from the server once. If `False`, the prompts list will be - fetched from the server on each call to `list_prompts()`. The cache can be invalidated - by calling `invalidate_prompts_cache()`. You should set this to `True` if you know the - server will not change its prompts list, because it can drastically improve latency + cache_prompts_list: Whether to cache the prompts list. If `True`, the prompts list + will be cached and only fetched from the server once. If `False`, the prompts + list will be fetched from the server on each call to `list_prompts()`. + The cache can be invalidated by calling `invalidate_prompts_cache()`. + You should set this to `True` if you know the server will not change + its prompts list, because it can drastically improve latency (by avoiding a round-trip to the server every time). name: A readable name for the server. If not provided, we'll create one from the @@ -480,11 +482,12 @@ def __init__( if you know the server will not change its tools list, because it can drastically improve latency (by avoiding a round-trip to the server every time). - cache_prompts_list: Whether to cache the prompts list. If `True`, the prompts list will be - cached and only fetched from the server once. If `False`, the prompts list will be - fetched from the server on each call to `list_prompts()`. The cache can be invalidated - by calling `invalidate_prompts_cache()`. You should set this to `True` if you know the - server will not change its prompts list, because it can drastically improve latency + cache_prompts_list: Whether to cache the prompts list. If `True`, the prompts list + will be cached and only fetched from the server once. If `False`, the prompts + list will be fetched from the server on each call to `list_prompts()`. + The cache can be invalidated by calling `invalidate_prompts_cache()`. + You should set this to `True` if you know the server will not change + its prompts list, because it can drastically improve latency (by avoiding a round-trip to the server every time). name: A readable name for the server. If not provided, we'll create one from the @@ -575,11 +578,12 @@ def __init__( if you know the server will not change its tools list, because it can drastically improve latency (by avoiding a round-trip to the server every time). - cache_prompts_list: Whether to cache the prompts list. If `True`, the prompts list will be - cached and only fetched from the server once. If `False`, the prompts list will be - fetched from the server on each call to `list_prompts()`. The cache can be invalidated - by calling `invalidate_prompts_cache()`. You should set this to `True` if you know the - server will not change its prompts list, because it can drastically improve latency + cache_prompts_list: Whether to cache the prompts list. If `True`, the prompts list + will be cached and only fetched from the server once. If `False`, the prompts + list will be fetched from the server on each call to `list_prompts()`. + The cache can be invalidated by calling `invalidate_prompts_cache()`. + You should set this to `True` if you know the server will not change + its prompts list, because it can drastically improve latency (by avoiding a round-trip to the server every time). name: A readable name for the server. If not provided, we'll create one from the diff --git a/tests/mcp/test_caching.py b/tests/mcp/test_caching.py index 80fbb1898..e3f49bb88 100644 --- a/tests/mcp/test_caching.py +++ b/tests/mcp/test_caching.py @@ -69,8 +69,8 @@ async def test_server_caching_tools_works( async def test_server_caching_prompts_works( mock_list_prompts: AsyncMock, mock_initialize: AsyncMock, mock_stdio_client ): - """Test that if we turn caching on, the list of prompts is cached and not fetched from the server - on each call to `list_prompts()`. + """Test that if we turn caching on, the list of prompts is cached and not fetched + from the server on each call to `list_prompts()`. """ server = MCPServerStdio( params={ @@ -98,15 +98,18 @@ async def test_server_caching_prompts_works( result_prompts = await server.list_prompts() assert result_prompts == prompts - assert mock_list_prompts.call_count == 1, "list_prompts() should not have been called again" + assert mock_list_prompts.call_count == 1, ("list_prompts() " + "should not have been called again") # Invalidate the cache and call list_prompts() again server.invalidate_prompts_cache() result_prompts = await server.list_prompts() assert result_prompts == prompts - assert mock_list_prompts.call_count == 2, "list_prompts() should be called again" + assert mock_list_prompts.call_count == 2, ("list_prompts() " + "should be called again") - # Without invalidating the cache, calling list_prompts() again should return the cached value + # Without invalidating the cache, calling list_prompts() + # again should return the cached value result_prompts = await server.list_prompts() assert result_prompts == prompts From d07eb10543b36a611c3c1ce4fbdcc540c268b749 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Sat, 12 Jul 2025 13:06:42 +0400 Subject: [PATCH 05/10] Fixing unit tests --- examples/mcp/caching/main.py | 2 +- tests/mcp/helpers.py | 1 + tests/mcp/test_server_errors.py | 6 +++++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/examples/mcp/caching/main.py b/examples/mcp/caching/main.py index 724b0f972..3680c4ccc 100644 --- a/examples/mcp/caching/main.py +++ b/examples/mcp/caching/main.py @@ -22,7 +22,7 @@ async def run(mcp_server: MCPServerStreamableHttp): print("Cached prompts before invoking list_prompts") print(mcp_server._prompts_list) - print("\nCached prompts after invoking list_prompts") + print("Cached prompts after invoking list_prompts") await mcp_server.list_prompts() cached_prompts_list = mcp_server._prompts_list for prompt in cached_prompts_list.prompts: diff --git a/tests/mcp/helpers.py b/tests/mcp/helpers.py index 31d43c228..b6b259e2d 100644 --- a/tests/mcp/helpers.py +++ b/tests/mcp/helpers.py @@ -38,6 +38,7 @@ def __init__(self, tool_filter: ToolFilter, server_name: str): # Initialize parent class properly to avoid type errors super().__init__( cache_tools_list=False, + cache_prompts_list=False, client_session_timeout_seconds=None, tool_filter=tool_filter, ) diff --git a/tests/mcp/test_server_errors.py b/tests/mcp/test_server_errors.py index 9e0455115..03f3bcdb9 100644 --- a/tests/mcp/test_server_errors.py +++ b/tests/mcp/test_server_errors.py @@ -8,7 +8,11 @@ class CrashingClientSessionServer(_MCPServerWithClientSession): def __init__(self): - super().__init__(cache_tools_list=False, client_session_timeout_seconds=5) + super().__init__( + cache_tools_list=False, + cache_prompts_list=False, + client_session_timeout_seconds=5 + ) self.cleanup_called = False def create_streams(self): From 74ae4f7dfdad921e0660d26d99252fe7c7a83919 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Sat, 12 Jul 2025 13:13:57 +0400 Subject: [PATCH 06/10] Fixing unit tests --- tests/mcp/test_caching.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mcp/test_caching.py b/tests/mcp/test_caching.py index e3f49bb88..565bf1725 100644 --- a/tests/mcp/test_caching.py +++ b/tests/mcp/test_caching.py @@ -65,7 +65,7 @@ async def test_server_caching_tools_works( @pytest.mark.asyncio @patch("mcp.client.stdio.stdio_client", return_value=DummyStreamsContextManager()) @patch("mcp.client.session.ClientSession.initialize", new_callable=AsyncMock, return_value=None) -@patch("mcp.client.session.ClientSession.list_tools") +@patch("mcp.client.session.ClientSession.list_prompts") async def test_server_caching_prompts_works( mock_list_prompts: AsyncMock, mock_initialize: AsyncMock, mock_stdio_client ): From 8397b176436774bea581a32ab590ab7ca7737011 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Sat, 12 Jul 2025 13:38:15 +0400 Subject: [PATCH 07/10] Fixing unit tests --- examples/mcp/caching/main.py | 9 +++++++-- tests/mcp/test_caching.py | 11 ++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/examples/mcp/caching/main.py b/examples/mcp/caching/main.py index 3680c4ccc..475fca219 100644 --- a/examples/mcp/caching/main.py +++ b/examples/mcp/caching/main.py @@ -5,6 +5,8 @@ import time from typing import Any +from mcp.types import ListPromptsResult + from agents import gen_trace_id, trace from agents.mcp import MCPServerStreamableHttp @@ -25,8 +27,11 @@ async def run(mcp_server: MCPServerStreamableHttp): print("Cached prompts after invoking list_prompts") await mcp_server.list_prompts() cached_prompts_list = mcp_server._prompts_list - for prompt in cached_prompts_list.prompts: - print(f"name: {prompt.name}") + if isinstance(cached_prompts_list, ListPromptsResult): + for prompt in cached_prompts_list.prompts: + print(f"name: {prompt.name}") + else: + print("Failed to cache list_prompts") async def main(): async with MCPServerStreamableHttp( diff --git a/tests/mcp/test_caching.py b/tests/mcp/test_caching.py index 565bf1725..15d5a6992 100644 --- a/tests/mcp/test_caching.py +++ b/tests/mcp/test_caching.py @@ -84,19 +84,20 @@ async def test_server_caching_prompts_works( Prompt(name="prompt2"), ] - mock_list_prompts.return_value = ListPromptsResult(prompts=prompts) + list_prompts = ListPromptsResult(prompts=prompts) + mock_list_prompts.return_value = list_prompts async with server: # Call list_prompts() multiple times result_prompts = await server.list_prompts() - assert result_prompts == prompts + assert result_prompts == list_prompts assert mock_list_prompts.call_count == 1, "list_prompts() should have been called once" # Call list_prompts() again, should return the cached value result_prompts = await server.list_prompts() - assert result_prompts == prompts + assert result_prompts == list_prompts assert mock_list_prompts.call_count == 1, ("list_prompts() " "should not have been called again") @@ -104,7 +105,7 @@ async def test_server_caching_prompts_works( # Invalidate the cache and call list_prompts() again server.invalidate_prompts_cache() result_prompts = await server.list_prompts() - assert result_prompts == prompts + assert result_prompts == list_prompts assert mock_list_prompts.call_count == 2, ("list_prompts() " "should be called again") @@ -112,4 +113,4 @@ async def test_server_caching_prompts_works( # Without invalidating the cache, calling list_prompts() # again should return the cached value result_prompts = await server.list_prompts() - assert result_prompts == prompts + assert result_prompts == list_prompts From baece2ff8f56f33541de06e9bc176834c140fa33 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Sat, 12 Jul 2025 13:39:59 +0400 Subject: [PATCH 08/10] Fixing unit tests --- src/agents/mcp/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agents/mcp/server.py b/src/agents/mcp/server.py index 6a8434d64..05c3404b5 100644 --- a/src/agents/mcp/server.py +++ b/src/agents/mcp/server.py @@ -305,7 +305,7 @@ async def list_prompts( self._cache_dirty_prompts = False # Fetch the prompts from the server self._prompts_list = await self.session.list_prompts() - prompts = self._tools_list + prompts = self._prompts_list return prompts From 2e8b9046e1a64ce51e6397a2f0905ae286fcf026 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Sat, 12 Jul 2025 13:42:10 +0400 Subject: [PATCH 09/10] Fixing unit tests --- examples/mcp/caching/main.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/examples/mcp/caching/main.py b/examples/mcp/caching/main.py index 475fca219..07571b55c 100644 --- a/examples/mcp/caching/main.py +++ b/examples/mcp/caching/main.py @@ -5,7 +5,7 @@ import time from typing import Any -from mcp.types import ListPromptsResult +from mcp.types import ListPromptsResult, MCPTool from agents import gen_trace_id, trace from agents.mcp import MCPServerStreamableHttp @@ -18,8 +18,12 @@ async def run(mcp_server: MCPServerStreamableHttp): print("Cached tools names after invoking list_tools") await mcp_server.list_tools() cached_tools_list = mcp_server._tools_list - for tool in cached_tools_list: - print(f"name: {tool.name}") + if cached_tools_list: + for tool in cached_tools_list: + print(f"name: {tool.name}") + + else: + print("Failed to cache list_prompts") print("Cached prompts before invoking list_prompts") print(mcp_server._prompts_list) From 55be5b9a6dc21a87cf59bce4c6313ee6dea70b8e Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Sat, 12 Jul 2025 13:44:16 +0400 Subject: [PATCH 10/10] Fixing unit tests --- examples/mcp/caching/main.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/mcp/caching/main.py b/examples/mcp/caching/main.py index 07571b55c..b292b1910 100644 --- a/examples/mcp/caching/main.py +++ b/examples/mcp/caching/main.py @@ -5,8 +5,6 @@ import time from typing import Any -from mcp.types import ListPromptsResult, MCPTool - from agents import gen_trace_id, trace from agents.mcp import MCPServerStreamableHttp @@ -31,7 +29,7 @@ async def run(mcp_server: MCPServerStreamableHttp): print("Cached prompts after invoking list_prompts") await mcp_server.list_prompts() cached_prompts_list = mcp_server._prompts_list - if isinstance(cached_prompts_list, ListPromptsResult): + if cached_prompts_list: for prompt in cached_prompts_list.prompts: print(f"name: {prompt.name}") else: