From b7fbb777a9bfe3f024a2d680ce5ee1370a38b637 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Tue, 8 Jul 2025 23:05:03 +0400 Subject: [PATCH 01/52] Adding an example --- examples/mcp/resources_server/README.md | 29 +++++++ examples/mcp/resources_server/main.py | 65 +++++++++++++++ examples/mcp/resources_server/server.py | 106 ++++++++++++++++++++++++ src/agents/mcp/server.py | 103 +++++++++++++++++++++-- 4 files changed, 294 insertions(+), 9 deletions(-) create mode 100644 examples/mcp/resources_server/README.md create mode 100644 examples/mcp/resources_server/main.py create mode 100644 examples/mcp/resources_server/server.py diff --git a/examples/mcp/resources_server/README.md b/examples/mcp/resources_server/README.md new file mode 100644 index 000000000..c1b1c3b37 --- /dev/null +++ b/examples/mcp/resources_server/README.md @@ -0,0 +1,29 @@ +# MCP Prompt Server Example + +This example uses a local MCP prompt server in [server.py](server.py). + +Run the example via: + +``` +uv run python examples/mcp/prompt_server/main.py +``` + +## Details + +The example uses the `MCPServerStreamableHttp` class from `agents.mcp`. The server runs in a sub-process at `http://localhost:8000/mcp` and provides user-controlled prompts that generate agent instructions. + +The server exposes prompts like `generate_code_review_instructions` that take parameters such as focus area and programming language. The agent calls these prompts to dynamically generate its system instructions based on user-provided parameters. + +## Workflow + +The example demonstrates two key functions: + +1. **`show_available_prompts`** - Lists all available prompts on the MCP server, showing users what prompts they can select from. This demonstrates the discovery aspect of MCP prompts. + +2. **`demo_code_review`** - Shows the complete user-controlled prompt workflow: + - Calls `generate_code_review_instructions` with specific parameters (focus: "security vulnerabilities", language: "python") + - Uses the generated instructions to create an Agent with specialized code review capabilities + - Runs the agent against vulnerable sample code (command injection via `os.system`) + - The agent analyzes the code and provides security-focused feedback using available tools + +This pattern allows users to dynamically configure agent behavior through MCP prompts rather than hardcoded instructions. \ No newline at end of file diff --git a/examples/mcp/resources_server/main.py b/examples/mcp/resources_server/main.py new file mode 100644 index 000000000..522a1c865 --- /dev/null +++ b/examples/mcp/resources_server/main.py @@ -0,0 +1,65 @@ +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 MCPServer, MCPServerStreamableHttp +from mcp.types import ListResourcesResult + +async def list_resources(mcp_server: MCPServer) -> ListResourcesResult: + """List available resources """ + resources_result = await mcp_server.list_resources() + print("\n### Resources ###") + for resource in resources_result.resources: + print(f"name: {resource.name}, description: {resource.description}") + + print("\n") + +async def list_resources_templates(mcp_server: MCPServer) -> ListResourcesResult: + """List available resources templates """ + resources_templates_result = await mcp_server.list_resource_templates() + print("\n### Resource Templates ###") + for resource in resources_templates_result.resourceTemplates: + print(f"name: {resource.name}, description: {resource.description}") + + print("\n") + +async def main(): + async with MCPServerStreamableHttp( + name="Simple Prompt Server", + params={"url": "http://localhost:8000/mcp"}, + ) as server: + trace_id = gen_trace_id() + with trace(workflow_name="Simple Prompt Demo", trace_id=trace_id): + print(f"Trace: https://platform.openai.com/traces/trace?trace_id={trace_id}\n") + + await list_resources(server) + await list_resources_templates(server) + + +if __name__ == "__main__": + if not shutil.which("uv"): + raise RuntimeError("uv is not installed") + + 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 Simple Resources Server...") + process = subprocess.Popen(["uv", "run", server_file]) + time.sleep(3) + print("Server started\n") + except Exception as e: + print(f"Error starting server: {e}") + exit(1) + + try: + asyncio.run(main()) + finally: + if process: + process.terminate() + print("Server terminated.") diff --git a/examples/mcp/resources_server/server.py b/examples/mcp/resources_server/server.py new file mode 100644 index 000000000..af5fb38cd --- /dev/null +++ b/examples/mcp/resources_server/server.py @@ -0,0 +1,106 @@ +from mcp.server.fastmcp import FastMCP + + +mcp = FastMCP("Resources Server") + + +API_REFERENCE_MD = """ +# Company API Reference + +## Authentication +Use the `Authorization: Bearer ` header. + +### Endpoints +| Method | Path | Description | +|--------|--------------------|--------------------| +| GET | /users | List users | +| POST | /users | Create a new user | +| GET | /users/{{id}} | Retrieve a user | + +""" + +GETTING_STARTED_MD = """ +# Getting Started Guide + +Welcome! Follow these steps to get productive quickly. + +1. Sign up for an account. +2. Generate an API token. +3. Call `GET /users` to verify your setup. + +""" + +CHANGELOG_MD = """ +# Latest Changelog + +## v2.1.0 — 2025-07-01 +* Added OAuth 2.1 support +* Reduced request latency by 25 % +* Fixed edge-case bug in /reports endpoint +""" + +# ────────────────────────────────────────────────────────────────────── +# 1. Static resources +# ────────────────────────────────────────────────────────────────────── +@mcp.resource( + "docs://api/reference", + name="Company API Reference", + description=( + "Static Markdown reference covering authentication, every endpoint’s " + "method and path, request/response schema, and example payloads." + ), +) +def api_reference() -> str: + return API_REFERENCE_MD + + +@mcp.resource( + "docs://guides/getting-started", + name="Getting Started Guide", + description=( + "Introductory walkthrough for new developers: account creation, token " + "generation, first API call, and common troubleshooting tips." + ), +) +def getting_started() -> str: + return GETTING_STARTED_MD + + +# ────────────────────────────────────────────────────────────────────── +# 2. Dynamic (async) resource +# ────────────────────────────────────────────────────────────────────── +@mcp.resource( + "docs://changelog/latest", + name="Latest Changelog", + description=( + "Async resource that delivers the most recent release notes at read-time. " + "Useful for surfacing new features and bug fixes to the LLM." + ), +) +async def latest_changelog() -> str: + return CHANGELOG_MD + + +# ────────────────────────────────────────────────────────────────────── +# 3. Template resource +# ────────────────────────────────────────────────────────────────────── +@mcp.resource( + "docs://{section}/search", + name="Docs Search", + description=( + "Template resource enabling full-text search within a chosen docs section " + "(e.g., api, guides, changelog). The URI parameter {section} must match " + "the function argument." + ), +) +def docs_search(section: str) -> str: + database = { + "api": API_REFERENCE_MD, + "guides": GETTING_STARTED_MD, + "changelog": CHANGELOG_MD, + } + return database.get(section, "Section not found.") + +if __name__ == "__main__": + # Initialize and run the server + mcp.run(transport='streamable-http') diff --git a/src/agents/mcp/server.py b/src/agents/mcp/server.py index 4fd606e34..f7fcdc8e8 100644 --- a/src/agents/mcp/server.py +++ b/src/agents/mcp/server.py @@ -6,6 +6,7 @@ from contextlib import AbstractAsyncContextManager, AsyncExitStack from datetime import timedelta from pathlib import Path +from pydantic import AnyUrl from typing import TYPE_CHECKING, Any, Literal, cast from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream @@ -13,7 +14,16 @@ from mcp.client.sse import sse_client from mcp.client.streamable_http import GetSessionIdCallback, streamablehttp_client from mcp.shared.message import SessionMessage -from mcp.types import CallToolResult, GetPromptResult, InitializeResult, ListPromptsResult +from mcp.types import ( + CallToolResult, + EmptyResult, + GetPromptResult, + InitializeResult, + ListPromptsResult, + ListResourcesResult, + ListResourceTemplatesResult, + ReadResourceResult +) from typing_extensions import NotRequired, TypedDict from ..exceptions import UserError @@ -77,6 +87,41 @@ async def get_prompt( """Get a specific prompt from the server.""" pass + @abc.abstractmethod + async def list_resources( + self, cursor: str | None = None + ) -> ListResourcesResult: + """List the resources available on the server.""" + pass + + @abc.abstractmethod + async def list_resource_templates( + self, cursor: str | None = None + ) -> ListResourceTemplatesResult: + """List the resources templates available on the server.""" + pass + + @abc.abstractmethod + async def read_resource( + self, uri: AnyUrl + ) -> ReadResourceResult: + """Read a specific resource given its uri.""" + pass + + @abc.abstractmethod + async def subscribe_resource( + self, uri: AnyUrl + ) -> EmptyResult: + """Subscribe to a specific resource given its uri.""" + pass + + @abc.abstractmethod + async def unsubscribe_resource( + self, uri: AnyUrl + ) -> EmptyResult: + """Unsubscribe from a specific resource given its uri.""" + pass + class _MCPServerWithClientSession(MCPServer, abc.ABC): """Base class for MCP servers that use a `ClientSession` to communicate with the server.""" @@ -113,6 +158,10 @@ def __init__( self.tool_filter = tool_filter + def _ensure_session(self) -> None: + if not self.session: + raise UserError("Server not initialized. Make sure you call connect() first.") + async def _apply_tool_filter( self, tools: list[MCPTool], @@ -247,8 +296,7 @@ async def list_tools( agent: Agent[Any] | None = None, ) -> list[MCPTool]: """List the tools available on the server.""" - if not self.session: - raise UserError("Server not initialized. Make sure you call `connect()` first.") + self._ensure_session() # 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: @@ -270,8 +318,7 @@ async def list_tools( async def call_tool(self, tool_name: str, arguments: dict[str, Any] | None) -> CallToolResult: """Invoke a tool on the server.""" - if not self.session: - raise UserError("Server not initialized. Make sure you call `connect()` first.") + self._ensure_session() return await self.session.call_tool(tool_name, arguments) @@ -279,8 +326,7 @@ async def list_prompts( self, ) -> ListPromptsResult: """List the prompts available on the server.""" - if not self.session: - raise UserError("Server not initialized. Make sure you call `connect()` first.") + self._ensure_session() return await self.session.list_prompts() @@ -288,11 +334,50 @@ async def get_prompt( self, name: str, arguments: dict[str, Any] | None = None ) -> GetPromptResult: """Get a specific prompt from the server.""" - if not self.session: - raise UserError("Server not initialized. Make sure you call `connect()` first.") + self._ensure_session() return await self.session.get_prompt(name, arguments) + async def list_resources( + self, cursor: str | None = None + ) -> ListResourcesResult: + """List the resources available on the server.""" + self._ensure_session() + + return await self.session.list_resources(cursor) + + async def list_resource_templates( + self, cursor: str | None = None + ) -> ListResourceTemplatesResult: + """List the resources templates available on the server.""" + self._ensure_session() + + return await self.session.list_resource_templates(cursor) + + async def read_resource( + self, uri: AnyUrl + ) -> ReadResourceResult: + """Read a specific resource given its uri.""" + self._ensure_session() + + return await self.session.read_resource(uri) + + async def subscribe_resource( + self, uri: AnyUrl + ) -> EmptyResult: + """Subscribe to a specific resource given its uri.""" + self._ensure_session() + + return await self.session.subscribe_resource(uri) + + async def unsubscribe_resource( + self, uri: AnyUrl + ) -> EmptyResult: + """Unsubscribe from a specific resource given its uri.""" + self._ensure_session() + + return await self.session.unsubscribe_resource(uri) + async def cleanup(self): """Cleanup the server.""" async with self._cleanup_lock: From 2ae7c3be5c3a14059f110b1de343448de75ed3df Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Tue, 8 Jul 2025 23:18:24 +0400 Subject: [PATCH 02/52] Finish resources examples --- examples/mcp/resources_server/main.py | 15 +++++++------ src/agents/mcp/server.py | 31 --------------------------- 2 files changed, 8 insertions(+), 38 deletions(-) diff --git a/examples/mcp/resources_server/main.py b/examples/mcp/resources_server/main.py index 522a1c865..e33a00f2b 100644 --- a/examples/mcp/resources_server/main.py +++ b/examples/mcp/resources_server/main.py @@ -1,5 +1,6 @@ import asyncio import os +from pydantic import AnyUrl import shutil import subprocess import time @@ -7,7 +8,7 @@ from agents import gen_trace_id, trace from agents.mcp import MCPServer, MCPServerStreamableHttp -from mcp.types import ListResourcesResult +from mcp.types import EmptyResult, ListResourcesResult, ReadResourceResult async def list_resources(mcp_server: MCPServer) -> ListResourcesResult: """List available resources """ @@ -16,16 +17,16 @@ async def list_resources(mcp_server: MCPServer) -> ListResourcesResult: for resource in resources_result.resources: print(f"name: {resource.name}, description: {resource.description}") - print("\n") - -async def list_resources_templates(mcp_server: MCPServer) -> ListResourcesResult: +async def list_resource_templates(mcp_server: MCPServer) -> ListResourcesResult: """List available resources templates """ resources_templates_result = await mcp_server.list_resource_templates() print("\n### Resource Templates ###") for resource in resources_templates_result.resourceTemplates: print(f"name: {resource.name}, description: {resource.description}") - print("\n") +async def read_resource(mcp_server: MCPServer, uri: AnyUrl) -> ReadResourceResult: + resource = await mcp_server.read_resource(uri) + print(resource.contents[0].text) async def main(): async with MCPServerStreamableHttp( @@ -37,8 +38,8 @@ async def main(): print(f"Trace: https://platform.openai.com/traces/trace?trace_id={trace_id}\n") await list_resources(server) - await list_resources_templates(server) - + await list_resource_templates(server) + await read_resource(server, AnyUrl("docs://api/reference")) if __name__ == "__main__": if not shutil.which("uv"): diff --git a/src/agents/mcp/server.py b/src/agents/mcp/server.py index f7fcdc8e8..c88083316 100644 --- a/src/agents/mcp/server.py +++ b/src/agents/mcp/server.py @@ -108,21 +108,6 @@ async def read_resource( """Read a specific resource given its uri.""" pass - @abc.abstractmethod - async def subscribe_resource( - self, uri: AnyUrl - ) -> EmptyResult: - """Subscribe to a specific resource given its uri.""" - pass - - @abc.abstractmethod - async def unsubscribe_resource( - self, uri: AnyUrl - ) -> EmptyResult: - """Unsubscribe from a specific resource given its uri.""" - pass - - class _MCPServerWithClientSession(MCPServer, abc.ABC): """Base class for MCP servers that use a `ClientSession` to communicate with the server.""" @@ -362,22 +347,6 @@ async def read_resource( return await self.session.read_resource(uri) - async def subscribe_resource( - self, uri: AnyUrl - ) -> EmptyResult: - """Subscribe to a specific resource given its uri.""" - self._ensure_session() - - return await self.session.subscribe_resource(uri) - - async def unsubscribe_resource( - self, uri: AnyUrl - ) -> EmptyResult: - """Unsubscribe from a specific resource given its uri.""" - self._ensure_session() - - return await self.session.unsubscribe_resource(uri) - async def cleanup(self): """Cleanup the server.""" async with self._cleanup_lock: From c2e9e4cbacac621f6bb9cd073002a545d6c25a75 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Tue, 8 Jul 2025 23:26:18 +0400 Subject: [PATCH 03/52] Add documentation --- examples/mcp/resources_server/README.md | 30 +++++++++---------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/examples/mcp/resources_server/README.md b/examples/mcp/resources_server/README.md index c1b1c3b37..5d3423ef5 100644 --- a/examples/mcp/resources_server/README.md +++ b/examples/mcp/resources_server/README.md @@ -1,29 +1,19 @@ -# MCP Prompt Server Example +# MCP Resources Server Example +This example shows the absolute basics of working with an MCP resources server by discovering what resources exist and reading one of them. -This example uses a local MCP prompt server in [server.py](server.py). +The local MCP Resources Server is defined in [server.py](server.py). Run the example via: ``` -uv run python examples/mcp/prompt_server/main.py +uv run python examples/mcp/resources_server/main.py ``` -## Details +## What the code does -The example uses the `MCPServerStreamableHttp` class from `agents.mcp`. The server runs in a sub-process at `http://localhost:8000/mcp` and provides user-controlled prompts that generate agent instructions. +The example uses the `MCPServerStreamableHttp` class from `agents.mcp`. The server runs in a sub-process at `http://localhost:8000/mcp` and provides resources that can be exposed to agents. +The example demonstrates three main functions: -The server exposes prompts like `generate_code_review_instructions` that take parameters such as focus area and programming language. The agent calls these prompts to dynamically generate its system instructions based on user-provided parameters. - -## Workflow - -The example demonstrates two key functions: - -1. **`show_available_prompts`** - Lists all available prompts on the MCP server, showing users what prompts they can select from. This demonstrates the discovery aspect of MCP prompts. - -2. **`demo_code_review`** - Shows the complete user-controlled prompt workflow: - - Calls `generate_code_review_instructions` with specific parameters (focus: "security vulnerabilities", language: "python") - - Uses the generated instructions to create an Agent with specialized code review capabilities - - Runs the agent against vulnerable sample code (command injection via `os.system`) - - The agent analyzes the code and provides security-focused feedback using available tools - -This pattern allows users to dynamically configure agent behavior through MCP prompts rather than hardcoded instructions. \ No newline at end of file +1. **`list_resources`** - Lists all available resources in the MCP server. +2. **`list_resource_templates`** - Lists all available resources templates in the MCP server. +3. **`read_resource`** - Read a specific resource from the MCP server. From 1b1a55f317b03ed43816a5e5e978ae625eb06938 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Tue, 8 Jul 2025 23:40:12 +0400 Subject: [PATCH 04/52] Update global docs --- docs/mcp.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/docs/mcp.md b/docs/mcp.md index eef61a047..11a895ef2 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -167,6 +167,42 @@ agent = Agent( ) ``` +## Resources + +Resources are a core primitive in the Model Context Protocol (MCP) that allow servers to expose data and content that can be read by clients and used as context for LLM interactions. + +### Using Resources + +MCP servers that support resources provide three main methods: + +- `list_resources()`: Lists all available resources on the server +- `list_resource_templates()`: Lists all available resources templates on the server +- `read_resource()`: Read data from a specific resource given its URI + +```python +# List available resources +resources_result = await mcp_server.list_resources() +for resource in resources_result.resources: + print(f"name: {resource.name}, description: {resource.description}") + +# List available resources templates +resources_templates_result = await mcp_server.list_resource_templates() +for resource in resources_templates_result.resourceTemplates: + print(f"name: {resource.name}, description: {resource.description}") + +# Read from a specific resource +resource = await mcp_server.read_resource("docs://api/reference") +print(resource.contents[0].text) + +# Use the prompt-generated instructions with an Agent +agent = Agent( + name="Company Information Maintainer", + instructions="How to access to API service?", + mcp_servers=[server] +) +``` + + ## 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. From a812d240af7c6020aa8866018148add70003333e Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Tue, 8 Jul 2025 23:48:37 +0400 Subject: [PATCH 05/52] Linting --- examples/mcp/resources_server/main.py | 9 +++++++-- examples/mcp/resources_server/server.py | 4 +++- src/agents/mcp/server.py | 24 ++++++++---------------- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/examples/mcp/resources_server/main.py b/examples/mcp/resources_server/main.py index e33a00f2b..5c96ab866 100644 --- a/examples/mcp/resources_server/main.py +++ b/examples/mcp/resources_server/main.py @@ -10,24 +10,28 @@ from agents.mcp import MCPServer, MCPServerStreamableHttp from mcp.types import EmptyResult, ListResourcesResult, ReadResourceResult + async def list_resources(mcp_server: MCPServer) -> ListResourcesResult: - """List available resources """ + """List available resources""" resources_result = await mcp_server.list_resources() print("\n### Resources ###") for resource in resources_result.resources: print(f"name: {resource.name}, description: {resource.description}") + async def list_resource_templates(mcp_server: MCPServer) -> ListResourcesResult: - """List available resources templates """ + """List available resources templates""" resources_templates_result = await mcp_server.list_resource_templates() print("\n### Resource Templates ###") for resource in resources_templates_result.resourceTemplates: print(f"name: {resource.name}, description: {resource.description}") + async def read_resource(mcp_server: MCPServer, uri: AnyUrl) -> ReadResourceResult: resource = await mcp_server.read_resource(uri) print(resource.contents[0].text) + async def main(): async with MCPServerStreamableHttp( name="Simple Prompt Server", @@ -41,6 +45,7 @@ async def main(): await list_resource_templates(server) await read_resource(server, AnyUrl("docs://api/reference")) + if __name__ == "__main__": if not shutil.which("uv"): raise RuntimeError("uv is not installed") diff --git a/examples/mcp/resources_server/server.py b/examples/mcp/resources_server/server.py index af5fb38cd..c80a36767 100644 --- a/examples/mcp/resources_server/server.py +++ b/examples/mcp/resources_server/server.py @@ -39,6 +39,7 @@ * Fixed edge-case bug in /reports endpoint """ + # ────────────────────────────────────────────────────────────────────── # 1. Static resources # ────────────────────────────────────────────────────────────────────── @@ -101,6 +102,7 @@ def docs_search(section: str) -> str: } return database.get(section, "Section not found.") + if __name__ == "__main__": # Initialize and run the server - mcp.run(transport='streamable-http') + mcp.run(transport="streamable-http") diff --git a/src/agents/mcp/server.py b/src/agents/mcp/server.py index c88083316..024871c22 100644 --- a/src/agents/mcp/server.py +++ b/src/agents/mcp/server.py @@ -16,13 +16,12 @@ from mcp.shared.message import SessionMessage from mcp.types import ( CallToolResult, - EmptyResult, GetPromptResult, InitializeResult, ListPromptsResult, ListResourcesResult, ListResourceTemplatesResult, - ReadResourceResult + ReadResourceResult, ) from typing_extensions import NotRequired, TypedDict @@ -88,26 +87,23 @@ async def get_prompt( pass @abc.abstractmethod - async def list_resources( - self, cursor: str | None = None - ) -> ListResourcesResult: + async def list_resources(self, cursor: str | None = None) -> ListResourcesResult: """List the resources available on the server.""" pass @abc.abstractmethod async def list_resource_templates( - self, cursor: str | None = None + self, cursor: str | None = None ) -> ListResourceTemplatesResult: """List the resources templates available on the server.""" pass @abc.abstractmethod - async def read_resource( - self, uri: AnyUrl - ) -> ReadResourceResult: + async def read_resource(self, uri: AnyUrl) -> ReadResourceResult: """Read a specific resource given its uri.""" pass + class _MCPServerWithClientSession(MCPServer, abc.ABC): """Base class for MCP servers that use a `ClientSession` to communicate with the server.""" @@ -323,25 +319,21 @@ async def get_prompt( return await self.session.get_prompt(name, arguments) - async def list_resources( - self, cursor: str | None = None - ) -> ListResourcesResult: + async def list_resources(self, cursor: str | None = None) -> ListResourcesResult: """List the resources available on the server.""" self._ensure_session() return await self.session.list_resources(cursor) async def list_resource_templates( - self, cursor: str | None = None + self, cursor: str | None = None ) -> ListResourceTemplatesResult: """List the resources templates available on the server.""" self._ensure_session() return await self.session.list_resource_templates(cursor) - async def read_resource( - self, uri: AnyUrl - ) -> ReadResourceResult: + async def read_resource(self, uri: AnyUrl) -> ReadResourceResult: """Read a specific resource given its uri.""" self._ensure_session() From 0be886e08097b447c9e41dee0def2f416323fda5 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Tue, 8 Jul 2025 23:51:32 +0400 Subject: [PATCH 06/52] Linting --- examples/mcp/resources_server/main.py | 8 ++------ examples/mcp/resources_server/server.py | 7 ------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/examples/mcp/resources_server/main.py b/examples/mcp/resources_server/main.py index 5c96ab866..39d676498 100644 --- a/examples/mcp/resources_server/main.py +++ b/examples/mcp/resources_server/main.py @@ -1,16 +1,16 @@ import asyncio import os -from pydantic import AnyUrl import shutil import subprocess import time from typing import Any +from pydantic import AnyUrl + from agents import gen_trace_id, trace from agents.mcp import MCPServer, MCPServerStreamableHttp from mcp.types import EmptyResult, ListResourcesResult, ReadResourceResult - async def list_resources(mcp_server: MCPServer) -> ListResourcesResult: """List available resources""" resources_result = await mcp_server.list_resources() @@ -18,7 +18,6 @@ async def list_resources(mcp_server: MCPServer) -> ListResourcesResult: for resource in resources_result.resources: print(f"name: {resource.name}, description: {resource.description}") - async def list_resource_templates(mcp_server: MCPServer) -> ListResourcesResult: """List available resources templates""" resources_templates_result = await mcp_server.list_resource_templates() @@ -26,12 +25,10 @@ async def list_resource_templates(mcp_server: MCPServer) -> ListResourcesResult: for resource in resources_templates_result.resourceTemplates: print(f"name: {resource.name}, description: {resource.description}") - async def read_resource(mcp_server: MCPServer, uri: AnyUrl) -> ReadResourceResult: resource = await mcp_server.read_resource(uri) print(resource.contents[0].text) - async def main(): async with MCPServerStreamableHttp( name="Simple Prompt Server", @@ -45,7 +42,6 @@ async def main(): await list_resource_templates(server) await read_resource(server, AnyUrl("docs://api/reference")) - if __name__ == "__main__": if not shutil.which("uv"): raise RuntimeError("uv is not installed") diff --git a/examples/mcp/resources_server/server.py b/examples/mcp/resources_server/server.py index c80a36767..f8fdad691 100644 --- a/examples/mcp/resources_server/server.py +++ b/examples/mcp/resources_server/server.py @@ -1,9 +1,7 @@ from mcp.server.fastmcp import FastMCP - mcp = FastMCP("Resources Server") - API_REFERENCE_MD = """ # Company API Reference @@ -39,7 +37,6 @@ * Fixed edge-case bug in /reports endpoint """ - # ────────────────────────────────────────────────────────────────────── # 1. Static resources # ────────────────────────────────────────────────────────────────────── @@ -54,7 +51,6 @@ def api_reference() -> str: return API_REFERENCE_MD - @mcp.resource( "docs://guides/getting-started", name="Getting Started Guide", @@ -66,7 +62,6 @@ def api_reference() -> str: def getting_started() -> str: return GETTING_STARTED_MD - # ────────────────────────────────────────────────────────────────────── # 2. Dynamic (async) resource # ────────────────────────────────────────────────────────────────────── @@ -81,7 +76,6 @@ def getting_started() -> str: async def latest_changelog() -> str: return CHANGELOG_MD - # ────────────────────────────────────────────────────────────────────── # 3. Template resource # ────────────────────────────────────────────────────────────────────── @@ -102,7 +96,6 @@ def docs_search(section: str) -> str: } return database.get(section, "Section not found.") - if __name__ == "__main__": # Initialize and run the server mcp.run(transport="streamable-http") From 369da53aa84c243ad0fb761d604fea608c3e0781 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Tue, 8 Jul 2025 23:54:28 +0400 Subject: [PATCH 07/52] Fix lint --- examples/mcp/resources_server/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/mcp/resources_server/main.py b/examples/mcp/resources_server/main.py index 39d676498..6fb057fdd 100644 --- a/examples/mcp/resources_server/main.py +++ b/examples/mcp/resources_server/main.py @@ -5,11 +5,12 @@ import time from typing import Any +from mcp.types import ListResourcesResult, ReadResourceResult from pydantic import AnyUrl from agents import gen_trace_id, trace from agents.mcp import MCPServer, MCPServerStreamableHttp -from mcp.types import EmptyResult, ListResourcesResult, ReadResourceResult + async def list_resources(mcp_server: MCPServer) -> ListResourcesResult: """List available resources""" From adedd7ece8cff11c56dae2e0339d112c4ba9541b Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Tue, 8 Jul 2025 23:55:38 +0400 Subject: [PATCH 08/52] Fix lint --- 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 024871c22..e43b03972 100644 --- a/src/agents/mcp/server.py +++ b/src/agents/mcp/server.py @@ -6,7 +6,6 @@ from contextlib import AbstractAsyncContextManager, AsyncExitStack from datetime import timedelta from pathlib import Path -from pydantic import AnyUrl from typing import TYPE_CHECKING, Any, Literal, cast from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream @@ -23,6 +22,7 @@ ListResourceTemplatesResult, ReadResourceResult, ) +from pydantic import AnyUrl from typing_extensions import NotRequired, TypedDict from ..exceptions import UserError From b6ce45bfc04a65c5ef1a5487a80e480311933d24 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Tue, 8 Jul 2025 23:58:39 +0400 Subject: [PATCH 09/52] Fix types --- src/agents/mcp/server.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/agents/mcp/server.py b/src/agents/mcp/server.py index e43b03972..2b94d6d48 100644 --- a/src/agents/mcp/server.py +++ b/src/agents/mcp/server.py @@ -139,10 +139,6 @@ def __init__( self.tool_filter = tool_filter - def _ensure_session(self) -> None: - if not self.session: - raise UserError("Server not initialized. Make sure you call connect() first.") - async def _apply_tool_filter( self, tools: list[MCPTool], @@ -277,7 +273,8 @@ async def list_tools( agent: Agent[Any] | None = None, ) -> list[MCPTool]: """List the tools available on the server.""" - self._ensure_session() + if not self.session: + 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: @@ -299,7 +296,8 @@ async def list_tools( async def call_tool(self, tool_name: str, arguments: dict[str, Any] | None) -> CallToolResult: """Invoke a tool on the server.""" - self._ensure_session() + if not self.session: + raise UserError("Server not initialized. Make sure you call connect() first.") return await self.session.call_tool(tool_name, arguments) @@ -307,7 +305,8 @@ async def list_prompts( self, ) -> ListPromptsResult: """List the prompts available on the server.""" - self._ensure_session() + if not self.session: + raise UserError("Server not initialized. Make sure you call connect() first.") return await self.session.list_prompts() @@ -315,13 +314,15 @@ async def get_prompt( self, name: str, arguments: dict[str, Any] | None = None ) -> GetPromptResult: """Get a specific prompt from the server.""" - self._ensure_session() + if not self.session: + raise UserError("Server not initialized. Make sure you call connect() first.") return await self.session.get_prompt(name, arguments) async def list_resources(self, cursor: str | None = None) -> ListResourcesResult: """List the resources available on the server.""" - self._ensure_session() + if not self.session: + raise UserError("Server not initialized. Make sure you call connect() first.") return await self.session.list_resources(cursor) @@ -329,13 +330,15 @@ async def list_resource_templates( self, cursor: str | None = None ) -> ListResourceTemplatesResult: """List the resources templates available on the server.""" - self._ensure_session() + if not self.session: + raise UserError("Server not initialized. Make sure you call connect() first.") return await self.session.list_resource_templates(cursor) async def read_resource(self, uri: AnyUrl) -> ReadResourceResult: """Read a specific resource given its uri.""" - self._ensure_session() + if not self.session: + raise UserError("Server not initialized. Make sure you call connect() first.") return await self.session.read_resource(uri) From 6a919e14a0545c2546046db178e5d246a2fff761 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 00:01:56 +0400 Subject: [PATCH 10/52] Fix types --- examples/mcp/resources_server/main.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/mcp/resources_server/main.py b/examples/mcp/resources_server/main.py index 6fb057fdd..3e1953222 100644 --- a/examples/mcp/resources_server/main.py +++ b/examples/mcp/resources_server/main.py @@ -12,22 +12,22 @@ from agents.mcp import MCPServer, MCPServerStreamableHttp -async def list_resources(mcp_server: MCPServer) -> ListResourcesResult: +async def list_resources(mcp_server: MCPServer): """List available resources""" - resources_result = await mcp_server.list_resources() + resources_result: ListResourcesResult = await mcp_server.list_resources() print("\n### Resources ###") for resource in resources_result.resources: print(f"name: {resource.name}, description: {resource.description}") -async def list_resource_templates(mcp_server: MCPServer) -> ListResourcesResult: +async def list_resource_templates(mcp_server: MCPServer): """List available resources templates""" - resources_templates_result = await mcp_server.list_resource_templates() + resources_templates_result: ListResourcesResult = await mcp_server.list_resource_templates() print("\n### Resource Templates ###") for resource in resources_templates_result.resourceTemplates: print(f"name: {resource.name}, description: {resource.description}") -async def read_resource(mcp_server: MCPServer, uri: AnyUrl) -> ReadResourceResult: - resource = await mcp_server.read_resource(uri) +async def read_resource(mcp_server: MCPServer, uri: AnyUrl): + resource: ReadResourceResult = await mcp_server.read_resource(uri) print(resource.contents[0].text) async def main(): From db08e8c108d2bfb46a0531dc0c748e1cce76b537 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 00:03:33 +0400 Subject: [PATCH 11/52] Fix typing --- examples/mcp/resources_server/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/mcp/resources_server/main.py b/examples/mcp/resources_server/main.py index 3e1953222..16e4ea2e1 100644 --- a/examples/mcp/resources_server/main.py +++ b/examples/mcp/resources_server/main.py @@ -5,7 +5,7 @@ import time from typing import Any -from mcp.types import ListResourcesResult, ReadResourceResult +from mcp.types import ListResourcesResult, ListResourceTemplatesResult, ReadResourceResult from pydantic import AnyUrl from agents import gen_trace_id, trace @@ -21,7 +21,7 @@ async def list_resources(mcp_server: MCPServer): async def list_resource_templates(mcp_server: MCPServer): """List available resources templates""" - resources_templates_result: ListResourcesResult = await mcp_server.list_resource_templates() + resources_templates_result: ListResourceTemplatesResult = await mcp_server.list_resource_templates() print("\n### Resource Templates ###") for resource in resources_templates_result.resourceTemplates: print(f"name: {resource.name}, description: {resource.description}") From a4ddd01e06be002a247661844902c323affd0497 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 00:05:10 +0400 Subject: [PATCH 12/52] Fix typing --- examples/mcp/resources_server/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/mcp/resources_server/main.py b/examples/mcp/resources_server/main.py index 16e4ea2e1..f06d3fd0b 100644 --- a/examples/mcp/resources_server/main.py +++ b/examples/mcp/resources_server/main.py @@ -28,7 +28,8 @@ async def list_resource_templates(mcp_server: MCPServer): async def read_resource(mcp_server: MCPServer, uri: AnyUrl): resource: ReadResourceResult = await mcp_server.read_resource(uri) - print(resource.contents[0].text) + print("\n### Resource Content ###") + print(resource.contents[0]) async def main(): async with MCPServerStreamableHttp( From cfd050f66d09b3b8ab94c69d4e41d6c15f336b15 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 00:15:31 +0400 Subject: [PATCH 13/52] Fix typing --- tests/mcp/helpers.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/mcp/helpers.py b/tests/mcp/helpers.py index 31d43c228..6374d3c09 100644 --- a/tests/mcp/helpers.py +++ b/tests/mcp/helpers.py @@ -3,8 +3,18 @@ import shutil from typing import Any +from pydantic import AnyUrl + from mcp import Tool as MCPTool -from mcp.types import CallToolResult, GetPromptResult, ListPromptsResult, PromptMessage, TextContent +from mcp.types import ( + CallToolResult, + GetPromptResult, + InitializeResult, + ListPromptsResult, + ListResourcesResult, + ListResourceTemplatesResult, + ReadResourceResult, +) from agents.mcp import MCPServer from agents.mcp.server import _MCPServerWithClientSession @@ -106,6 +116,18 @@ async def get_prompt( message = PromptMessage(role="user", content=TextContent(type="text", text=content)) return GetPromptResult(description=f"Fake prompt: {name}", messages=[message]) + async def list_resources(self, run_context=None, agent=None) -> ListResourcesResult: + """Return empty list of resources for fake server""" + return ListResourcesResult(resources=[]) + + async def list_resource_templates(self, run_context=None, agent=None) -> ListResourceTemplatesResult: + """Return empty list of resources templates for fake server""" + return ListResourceTemplatesResult(resourceTemplates=[]) + + async def read_resource(self, uri: AnyUrl) -> ReadResourceResult: + """Return a fake resource read for fake server""" + return ReadResourceResult(contents=[]) + @property def name(self) -> str: return self._server_name From 7ab231af0a48ea85dbd650e64f64a2aa94994306 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 00:17:27 +0400 Subject: [PATCH 14/52] Fix typing --- tests/mcp/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mcp/helpers.py b/tests/mcp/helpers.py index 6374d3c09..c0094a08e 100644 --- a/tests/mcp/helpers.py +++ b/tests/mcp/helpers.py @@ -9,11 +9,11 @@ from mcp.types import ( CallToolResult, GetPromptResult, - InitializeResult, ListPromptsResult, ListResourcesResult, ListResourceTemplatesResult, ReadResourceResult, + TextContent ) from agents.mcp import MCPServer From 7e19afa40c04d403b9925c58e34ccff68762d8ce Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 00:24:16 +0400 Subject: [PATCH 15/52] Fix unit tests --- tests/mcp/helpers.py | 9 +++++---- tests/mcp/test_prompt_server.py | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/tests/mcp/helpers.py b/tests/mcp/helpers.py index c0094a08e..8d31f62ad 100644 --- a/tests/mcp/helpers.py +++ b/tests/mcp/helpers.py @@ -3,8 +3,6 @@ import shutil from typing import Any -from pydantic import AnyUrl - from mcp import Tool as MCPTool from mcp.types import ( CallToolResult, @@ -12,9 +10,11 @@ ListPromptsResult, ListResourcesResult, ListResourceTemplatesResult, + PromptMessage, ReadResourceResult, - TextContent + TextContent, ) +from pydantic import AnyUrl from agents.mcp import MCPServer from agents.mcp.server import _MCPServerWithClientSession @@ -120,7 +120,8 @@ async def list_resources(self, run_context=None, agent=None) -> ListResourcesRes """Return empty list of resources for fake server""" return ListResourcesResult(resources=[]) - async def list_resource_templates(self, run_context=None, agent=None) -> ListResourceTemplatesResult: + async def list_resource_templates(self, run_context=None, agent=None) \ + -> ListResourceTemplatesResult: """Return empty list of resources templates for fake server""" return ListResourceTemplatesResult(resourceTemplates=[]) diff --git a/tests/mcp/test_prompt_server.py b/tests/mcp/test_prompt_server.py index 15afe28e4..1f508f54b 100644 --- a/tests/mcp/test_prompt_server.py +++ b/tests/mcp/test_prompt_server.py @@ -1,6 +1,8 @@ from typing import Any import pytest +from mcp.types import ListResourcesResult, ListResourceTemplatesResult, ReadResourceResult +from pydantic import AnyUrl from agents import Agent, Runner from agents.mcp import MCPServer @@ -66,6 +68,19 @@ async def list_tools(self, run_context=None, agent=None): async def call_tool(self, tool_name: str, arguments: dict[str, Any] | None = None): raise NotImplementedError("This fake server doesn't support tools") + async def list_resources(self, run_context=None, agent=None) -> ListResourcesResult: + """Return empty list of resources for fake server""" + return ListResourcesResult(resources=[]) + + async def list_resource_templates(self, run_context=None, agent=None) \ + -> ListResourceTemplatesResult: + """Return empty list of resources templates for fake server""" + return ListResourceTemplatesResult(resourceTemplates=[]) + + async def read_resource(self, uri: AnyUrl) -> ReadResourceResult: + """Return a fake resource read for fake server""" + return ReadResourceResult(contents=[]) + @property def name(self) -> str: return self._server_name From 7d4a58b2eff4587f6b22567c4289eacdf59c5be2 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 00:35:00 +0400 Subject: [PATCH 16/52] Adding unit tests --- tests/mcp/helpers.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/mcp/helpers.py b/tests/mcp/helpers.py index 8d31f62ad..40f210a75 100644 --- a/tests/mcp/helpers.py +++ b/tests/mcp/helpers.py @@ -12,6 +12,8 @@ ListResourceTemplatesResult, PromptMessage, ReadResourceResult, + Resource, + ResourceTemplate, TextContent, ) from pydantic import AnyUrl @@ -67,10 +69,14 @@ def name(self) -> str: class FakeMCPServer(MCPServer): def __init__( self, + resources: ListResourcesResult = ListResourcesResult(resources=[]), + resources_templates: ListResourceTemplatesResult = ListResourceTemplatesResult(resourceTemplates=[]), tools: list[MCPTool] | None = None, tool_filter: ToolFilter = None, server_name: str = "fake_mcp_server", ): + self.resources = resources + self.resources_templates = resources_templates self.tools: list[MCPTool] = tools or [] self.tool_calls: list[str] = [] self.tool_results: list[str] = [] @@ -129,6 +135,12 @@ async def read_resource(self, uri: AnyUrl) -> ReadResourceResult: """Return a fake resource read for fake server""" return ReadResourceResult(contents=[]) + def add_resource(self, uri: AnyUrl, description: str | None = None): + self.resources.append(Resource(uri=uri, description=description)) + + def add_resource_template(self, uri: AnyUrl, description: str | None = None): + self.resources_templates.append(ResourceTemplate(uriTemplate=uri, description=description)) + @property def name(self) -> str: return self._server_name From c654f204720afdc4926375f662fdf29d318ca64c Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 00:38:32 +0400 Subject: [PATCH 17/52] Adding unit tests --- tests/mcp/helpers.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/mcp/helpers.py b/tests/mcp/helpers.py index 40f210a75..03d07435d 100644 --- a/tests/mcp/helpers.py +++ b/tests/mcp/helpers.py @@ -69,12 +69,16 @@ def name(self) -> str: class FakeMCPServer(MCPServer): def __init__( self, - resources: ListResourcesResult = ListResourcesResult(resources=[]), - resources_templates: ListResourceTemplatesResult = ListResourceTemplatesResult(resourceTemplates=[]), + resources: ListResourcesResult | None = None, + resources_templates: ListResourceTemplatesResult | None = None, tools: list[MCPTool] | None = None, tool_filter: ToolFilter = None, server_name: str = "fake_mcp_server", ): + if not resources: + resources = ListResourcesResult(resources=[]) + if not resources_templates: + resources_templates = ListResourceTemplatesResult(resourceTemplates=[]) self.resources = resources self.resources_templates = resources_templates self.tools: list[MCPTool] = tools or [] From 107b584e7af126cf71faef62804d8234ae5a1f84 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 00:42:21 +0400 Subject: [PATCH 18/52] Adding unit tests --- tests/mcp/helpers.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/mcp/helpers.py b/tests/mcp/helpers.py index 03d07435d..371d11f8a 100644 --- a/tests/mcp/helpers.py +++ b/tests/mcp/helpers.py @@ -75,12 +75,10 @@ def __init__( tool_filter: ToolFilter = None, server_name: str = "fake_mcp_server", ): - if not resources: - resources = ListResourcesResult(resources=[]) - if not resources_templates: - resources_templates = ListResourceTemplatesResult(resourceTemplates=[]) - self.resources = resources - self.resources_templates = resources_templates + self.resources = (resources or + ListResourcesResult(resources=[])) + self.resources_templates = (resources_templates or + ListResourceTemplatesResult(resourceTemplates=[])) self.tools: list[MCPTool] = tools or [] self.tool_calls: list[str] = [] self.tool_results: list[str] = [] @@ -139,11 +137,13 @@ async def read_resource(self, uri: AnyUrl) -> ReadResourceResult: """Return a fake resource read for fake server""" return ReadResourceResult(contents=[]) - def add_resource(self, uri: AnyUrl, description: str | None = None): - self.resources.append(Resource(uri=uri, description=description)) + def add_resource(self, uri: AnyUrl, name: str, description: str | None = None): + self.resources.resources.append(Resource(uri=uri, description=description, name=name)) - def add_resource_template(self, uri: AnyUrl, description: str | None = None): - self.resources_templates.append(ResourceTemplate(uriTemplate=uri, description=description)) + def add_resource_template(self, uri: AnyUrl, name: str, description: str | None = None): + self.resources_templates.resourceTemplates.append( + ResourceTemplate(uriTemplate=uri, description=description, name=name) + ) @property def name(self) -> str: From 27a04654312b7270693eb5784a5ce74b35c3b002 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 00:43:58 +0400 Subject: [PATCH 19/52] Adding unit tests --- tests/mcp/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mcp/helpers.py b/tests/mcp/helpers.py index 371d11f8a..b5e275365 100644 --- a/tests/mcp/helpers.py +++ b/tests/mcp/helpers.py @@ -140,7 +140,7 @@ async def read_resource(self, uri: AnyUrl) -> ReadResourceResult: def add_resource(self, uri: AnyUrl, name: str, description: str | None = None): self.resources.resources.append(Resource(uri=uri, description=description, name=name)) - def add_resource_template(self, uri: AnyUrl, name: str, description: str | None = None): + def add_resource_template(self, uri: str, name: str, description: str | None = None): self.resources_templates.resourceTemplates.append( ResourceTemplate(uriTemplate=uri, description=description, name=name) ) From b8eed9d4b9f1bb0532d08c119ca9fcea1aeccb7b Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 00:48:05 +0400 Subject: [PATCH 20/52] Adding unit tests --- tests/mcp/helpers.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/tests/mcp/helpers.py b/tests/mcp/helpers.py index b5e275365..634628986 100644 --- a/tests/mcp/helpers.py +++ b/tests/mcp/helpers.py @@ -69,16 +69,14 @@ def name(self) -> str: class FakeMCPServer(MCPServer): def __init__( self, - resources: ListResourcesResult | None = None, - resources_templates: ListResourceTemplatesResult | None = None, + resources: list[Resource] | None = None, + resources_templates: list[ResourceTemplate] | None = None, tools: list[MCPTool] | None = None, tool_filter: ToolFilter = None, server_name: str = "fake_mcp_server", ): - self.resources = (resources or - ListResourcesResult(resources=[])) - self.resources_templates = (resources_templates or - ListResourceTemplatesResult(resourceTemplates=[])) + self.resources = resources or [] + self.resources_templates = resources_templates or [] self.tools: list[MCPTool] = tools or [] self.tool_calls: list[str] = [] self.tool_results: list[str] = [] @@ -126,22 +124,22 @@ async def get_prompt( async def list_resources(self, run_context=None, agent=None) -> ListResourcesResult: """Return empty list of resources for fake server""" - return ListResourcesResult(resources=[]) + return ListResourcesResult(resources=self.resources) async def list_resource_templates(self, run_context=None, agent=None) \ -> ListResourceTemplatesResult: """Return empty list of resources templates for fake server""" - return ListResourceTemplatesResult(resourceTemplates=[]) + return ListResourceTemplatesResult(resourceTemplates=self.resources_templates) async def read_resource(self, uri: AnyUrl) -> ReadResourceResult: """Return a fake resource read for fake server""" return ReadResourceResult(contents=[]) def add_resource(self, uri: AnyUrl, name: str, description: str | None = None): - self.resources.resources.append(Resource(uri=uri, description=description, name=name)) + self.resources.append(Resource(uri=uri, description=description, name=name)) def add_resource_template(self, uri: str, name: str, description: str | None = None): - self.resources_templates.resourceTemplates.append( + self.resources_templates.append( ResourceTemplate(uriTemplate=uri, description=description, name=name) ) From 8a581c6ad0022a4ffa966a7554433431e01f5b29 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 00:59:30 +0400 Subject: [PATCH 21/52] Adding unit tests --- tests/mcp/test_resources_server.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 tests/mcp/test_resources_server.py diff --git a/tests/mcp/test_resources_server.py b/tests/mcp/test_resources_server.py new file mode 100644 index 000000000..e2c24c984 --- /dev/null +++ b/tests/mcp/test_resources_server.py @@ -0,0 +1,16 @@ +import pytest +from pydantic import AnyUrl + +from .helpers import FakeMCPServer + + +@pytest.mark.asyncio +async def test_list_resources(): + """Test listing available resources""" + server = FakeMCPServer() + server.add_resource(uri=AnyUrl("docs://api/reference"), name="reference") + + result = await server.list_resources() + assert len(result) == 1 + assert result.resources[0].uri == AnyUrl("docs://api/reference") + assert result.resources[0].name == "reference" From 28255acce423e0812d4fe95a9d03e34a379b029f Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 01:00:39 +0400 Subject: [PATCH 22/52] Adding unit tests --- tests/mcp/test_resources_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mcp/test_resources_server.py b/tests/mcp/test_resources_server.py index e2c24c984..b3ca81a5a 100644 --- a/tests/mcp/test_resources_server.py +++ b/tests/mcp/test_resources_server.py @@ -11,6 +11,6 @@ async def test_list_resources(): server.add_resource(uri=AnyUrl("docs://api/reference"), name="reference") result = await server.list_resources() - assert len(result) == 1 + assert len(result.resources) == 1 assert result.resources[0].uri == AnyUrl("docs://api/reference") assert result.resources[0].name == "reference" From 68a1924aee2c09ebf47ec50f5488532f9d0ba6b3 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 01:07:58 +0400 Subject: [PATCH 23/52] Adding unit tests --- tests/mcp/test_resources_server.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/mcp/test_resources_server.py b/tests/mcp/test_resources_server.py index b3ca81a5a..9c2ff3720 100644 --- a/tests/mcp/test_resources_server.py +++ b/tests/mcp/test_resources_server.py @@ -14,3 +14,14 @@ async def test_list_resources(): assert len(result.resources) == 1 assert result.resources[0].uri == AnyUrl("docs://api/reference") assert result.resources[0].name == "reference" + +@pytest.mark.asyncio +async def test_list_resource_templates(): + """Test listing available resource templates""" + server = FakeMCPServer() + server.add_resource_template(uri="docs://{section}/search", name="Docs Search") + + result = await server.list_resource_templates() + assert len(result.resourceTemplates) == 1 + assert result.resourceTemplates[0].uri == "docs://{section}/search" + assert result.resourceTemplates[0].name == "Docs Search" From 129dad54c2c127d644297c8bc38a843e8399368e Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 01:09:31 +0400 Subject: [PATCH 24/52] Adding unit tests --- tests/mcp/test_resources_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mcp/test_resources_server.py b/tests/mcp/test_resources_server.py index 9c2ff3720..70769771f 100644 --- a/tests/mcp/test_resources_server.py +++ b/tests/mcp/test_resources_server.py @@ -23,5 +23,5 @@ async def test_list_resource_templates(): result = await server.list_resource_templates() assert len(result.resourceTemplates) == 1 - assert result.resourceTemplates[0].uri == "docs://{section}/search" + assert result.resourceTemplates[0].uriTemplate == "docs://{section}/search" assert result.resourceTemplates[0].name == "Docs Search" From 649f8614bf8dec4c1ebaa2747911351af8b55866 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 01:48:57 +0400 Subject: [PATCH 25/52] Improve example --- examples/mcp/resources_server/main.py | 14 +++++++++++++- tests/mcp/test_resources_server.py | 3 ++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/examples/mcp/resources_server/main.py b/examples/mcp/resources_server/main.py index f06d3fd0b..3dc66386e 100644 --- a/examples/mcp/resources_server/main.py +++ b/examples/mcp/resources_server/main.py @@ -8,7 +8,7 @@ from mcp.types import ListResourcesResult, ListResourceTemplatesResult, ReadResourceResult from pydantic import AnyUrl -from agents import gen_trace_id, trace +from agents import Agent, Runner, gen_trace_id, trace from agents.mcp import MCPServer, MCPServerStreamableHttp @@ -44,6 +44,18 @@ async def main(): await list_resource_templates(server) await read_resource(server, AnyUrl("docs://api/reference")) + agent = Agent( + name="Assistant", + instructions="Answer users queries using the available resources", + mcp_servers=[server], + ) + + message = "What's the process to access the APIs? What are the available endpoints?" + print("\n" + "-" * 40) + print(f"Running: {message}") + result = await Runner.run(starting_agent=agent, input=message) + print(result.final_output) + if __name__ == "__main__": if not shutil.which("uv"): raise RuntimeError("uv is not installed") diff --git a/tests/mcp/test_resources_server.py b/tests/mcp/test_resources_server.py index 70769771f..fec0da98b 100644 --- a/tests/mcp/test_resources_server.py +++ b/tests/mcp/test_resources_server.py @@ -20,8 +20,9 @@ async def test_list_resource_templates(): """Test listing available resource templates""" server = FakeMCPServer() server.add_resource_template(uri="docs://{section}/search", name="Docs Search") + server.add_resource_template(uri="api://{router}/get", name="APIs Search") result = await server.list_resource_templates() - assert len(result.resourceTemplates) == 1 + assert len(result.resourceTemplates) == 2 assert result.resourceTemplates[0].uriTemplate == "docs://{section}/search" assert result.resourceTemplates[0].name == "Docs Search" From 5dc6cafe83ae3f8a00fe1ac817084c39d0c57b60 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 01:55:33 +0400 Subject: [PATCH 26/52] Adding unit tests --- tests/mcp/helpers.py | 6 +++++- tests/mcp/test_resources_server.py | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/mcp/helpers.py b/tests/mcp/helpers.py index 634628986..70004d7d1 100644 --- a/tests/mcp/helpers.py +++ b/tests/mcp/helpers.py @@ -133,7 +133,11 @@ async def list_resource_templates(self, run_context=None, agent=None) \ async def read_resource(self, uri: AnyUrl) -> ReadResourceResult: """Return a fake resource read for fake server""" - return ReadResourceResult(contents=[]) + for resource in self.resources: + if resource.uri == uri: + return resource + + raise KeyError def add_resource(self, uri: AnyUrl, name: str, description: str | None = None): self.resources.append(Resource(uri=uri, description=description, name=name)) diff --git a/tests/mcp/test_resources_server.py b/tests/mcp/test_resources_server.py index fec0da98b..3289fdef6 100644 --- a/tests/mcp/test_resources_server.py +++ b/tests/mcp/test_resources_server.py @@ -26,3 +26,11 @@ async def test_list_resource_templates(): assert len(result.resourceTemplates) == 2 assert result.resourceTemplates[0].uriTemplate == "docs://{section}/search" assert result.resourceTemplates[0].name == "Docs Search" + +@pytest.mark.asyncio +async def test_read_resource(): + server = FakeMCPServer() + server.add_resource_template(uri="docs://{section}/search", name="Docs Search") + + result = await server.get_prompt("docs://{section}/search") + assert result.name == "Docs Search" From 4e3b0fcf3436a5213b6e77cfcf91aa82c0654e1d Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 01:57:11 +0400 Subject: [PATCH 27/52] Adding unit tests --- tests/mcp/test_resources_server.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/mcp/test_resources_server.py b/tests/mcp/test_resources_server.py index 3289fdef6..553210a2a 100644 --- a/tests/mcp/test_resources_server.py +++ b/tests/mcp/test_resources_server.py @@ -29,8 +29,9 @@ async def test_list_resource_templates(): @pytest.mark.asyncio async def test_read_resource(): + """Test getting a resource""" server = FakeMCPServer() server.add_resource_template(uri="docs://{section}/search", name="Docs Search") - result = await server.get_prompt("docs://{section}/search") + result = await server.read_resource("docs://{section}/search") assert result.name == "Docs Search" From 4e37bdea51c0f26789efe600943c106c12eb8f98 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 01:57:36 +0400 Subject: [PATCH 28/52] Adding unit tests --- tests/mcp/test_resources_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mcp/test_resources_server.py b/tests/mcp/test_resources_server.py index 553210a2a..ddf0034bb 100644 --- a/tests/mcp/test_resources_server.py +++ b/tests/mcp/test_resources_server.py @@ -33,5 +33,5 @@ async def test_read_resource(): server = FakeMCPServer() server.add_resource_template(uri="docs://{section}/search", name="Docs Search") - result = await server.read_resource("docs://{section}/search") + result = await server.read_resource(AnyUrl("docs://{section}/search")) assert result.name == "Docs Search" From b4cf67cb03070d0a40da3f1dbc1244c931713fe4 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 02:00:04 +0400 Subject: [PATCH 29/52] Adding unit tests --- tests/mcp/test_resources_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mcp/test_resources_server.py b/tests/mcp/test_resources_server.py index ddf0034bb..c9c62e9d0 100644 --- a/tests/mcp/test_resources_server.py +++ b/tests/mcp/test_resources_server.py @@ -31,7 +31,7 @@ async def test_list_resource_templates(): async def test_read_resource(): """Test getting a resource""" server = FakeMCPServer() - server.add_resource_template(uri="docs://{section}/search", name="Docs Search") + server.add_resource(AnyUrl("docs://{section}/search"), name="Docs Search") result = await server.read_resource(AnyUrl("docs://{section}/search")) assert result.name == "Docs Search" From 8382b848e8d38efa0a0bd7621ca97e4850c841a0 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 02:01:50 +0400 Subject: [PATCH 30/52] Adding unit tests --- tests/mcp/test_resources_server.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/mcp/test_resources_server.py b/tests/mcp/test_resources_server.py index c9c62e9d0..cd597cbea 100644 --- a/tests/mcp/test_resources_server.py +++ b/tests/mcp/test_resources_server.py @@ -31,7 +31,16 @@ async def test_list_resource_templates(): async def test_read_resource(): """Test getting a resource""" server = FakeMCPServer() - server.add_resource(AnyUrl("docs://{section}/search"), name="Docs Search") + server.add_resource(AnyUrl("docs://api/reference"), name="Docs Search") + + result = await server.read_resource(AnyUrl("docs://api/reference")) + assert result.name == "Docs Search" + +@pytest.mark.asyncio +async def test_read_template_resource(): + """Test getting a resource""" + server = FakeMCPServer() + server.add_resource_template(uri="docs://{section}/search", name="Docs Search") result = await server.read_resource(AnyUrl("docs://{section}/search")) assert result.name == "Docs Search" From 2c09122d30a0e5e2d58c95e3b314cb5b07eecc5e Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 02:02:29 +0400 Subject: [PATCH 31/52] Adding unit tests --- tests/mcp/test_resources_server.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/mcp/test_resources_server.py b/tests/mcp/test_resources_server.py index cd597cbea..2685b6661 100644 --- a/tests/mcp/test_resources_server.py +++ b/tests/mcp/test_resources_server.py @@ -33,14 +33,12 @@ async def test_read_resource(): server = FakeMCPServer() server.add_resource(AnyUrl("docs://api/reference"), name="Docs Search") - result = await server.read_resource(AnyUrl("docs://api/reference")) - assert result.name == "Docs Search" + await server.read_resource(AnyUrl("docs://api/reference")) @pytest.mark.asyncio async def test_read_template_resource(): - """Test getting a resource""" + """Test getting a template resource""" server = FakeMCPServer() server.add_resource_template(uri="docs://{section}/search", name="Docs Search") - result = await server.read_resource(AnyUrl("docs://{section}/search")) - assert result.name == "Docs Search" + await server.read_resource(AnyUrl("docs://{section}/search")) From aaef281ee1cb0f83ff1491050d3dafa2f7904a3e Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 02:03:46 +0400 Subject: [PATCH 32/52] Adding unit tests --- tests/mcp/test_resources_server.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/mcp/test_resources_server.py b/tests/mcp/test_resources_server.py index 2685b6661..bf3b81f0e 100644 --- a/tests/mcp/test_resources_server.py +++ b/tests/mcp/test_resources_server.py @@ -34,11 +34,3 @@ async def test_read_resource(): server.add_resource(AnyUrl("docs://api/reference"), name="Docs Search") await server.read_resource(AnyUrl("docs://api/reference")) - -@pytest.mark.asyncio -async def test_read_template_resource(): - """Test getting a template resource""" - server = FakeMCPServer() - server.add_resource_template(uri="docs://{section}/search", name="Docs Search") - - await server.read_resource(AnyUrl("docs://{section}/search")) From c23735cea968168d25f390d801fd6a65abd32353 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 02:06:28 +0400 Subject: [PATCH 33/52] Adding unit tests --- tests/mcp/test_resources_server.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/mcp/test_resources_server.py b/tests/mcp/test_resources_server.py index bf3b81f0e..fe5bb8b35 100644 --- a/tests/mcp/test_resources_server.py +++ b/tests/mcp/test_resources_server.py @@ -34,3 +34,11 @@ async def test_read_resource(): server.add_resource(AnyUrl("docs://api/reference"), name="Docs Search") await server.read_resource(AnyUrl("docs://api/reference")) + +@pytest.mark.asyncio +async def test_get_prompt_not_found(): + """Test getting a prompt that doesn't exist""" + server = FakeMCPServer() + uri = "docs://api/reference" + with pytest.raises(ValueError, match=f"Resource {uri} not found"): + await server.read_resource(AnyUrl(uri)) From b2fa63ca9a0a102744a492d4bd4b3b68207b5b68 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 02:09:25 +0400 Subject: [PATCH 34/52] Adding unit tests --- tests/mcp/helpers.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/mcp/helpers.py b/tests/mcp/helpers.py index 70004d7d1..416c21ce5 100644 --- a/tests/mcp/helpers.py +++ b/tests/mcp/helpers.py @@ -12,7 +12,6 @@ ListResourceTemplatesResult, PromptMessage, ReadResourceResult, - Resource, ResourceTemplate, TextContent, ) @@ -69,7 +68,7 @@ def name(self) -> str: class FakeMCPServer(MCPServer): def __init__( self, - resources: list[Resource] | None = None, + resources: list[ReadResourceResult] | None = None, resources_templates: list[ResourceTemplate] | None = None, tools: list[MCPTool] | None = None, tool_filter: ToolFilter = None, @@ -140,7 +139,7 @@ async def read_resource(self, uri: AnyUrl) -> ReadResourceResult: raise KeyError def add_resource(self, uri: AnyUrl, name: str, description: str | None = None): - self.resources.append(Resource(uri=uri, description=description, name=name)) + self.resources.append(ReadResourceResult(uri=uri, description=description, name=name)) def add_resource_template(self, uri: str, name: str, description: str | None = None): self.resources_templates.append( From e0f8ee2d07ef5cb94aeb6b1c937a4f7976bbc4e4 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 02:13:32 +0400 Subject: [PATCH 35/52] Adding unit tests --- tests/mcp/helpers.py | 5 +++-- tests/mcp/test_resources_server.py | 8 -------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/tests/mcp/helpers.py b/tests/mcp/helpers.py index 416c21ce5..70004d7d1 100644 --- a/tests/mcp/helpers.py +++ b/tests/mcp/helpers.py @@ -12,6 +12,7 @@ ListResourceTemplatesResult, PromptMessage, ReadResourceResult, + Resource, ResourceTemplate, TextContent, ) @@ -68,7 +69,7 @@ def name(self) -> str: class FakeMCPServer(MCPServer): def __init__( self, - resources: list[ReadResourceResult] | None = None, + resources: list[Resource] | None = None, resources_templates: list[ResourceTemplate] | None = None, tools: list[MCPTool] | None = None, tool_filter: ToolFilter = None, @@ -139,7 +140,7 @@ async def read_resource(self, uri: AnyUrl) -> ReadResourceResult: raise KeyError def add_resource(self, uri: AnyUrl, name: str, description: str | None = None): - self.resources.append(ReadResourceResult(uri=uri, description=description, name=name)) + self.resources.append(Resource(uri=uri, description=description, name=name)) def add_resource_template(self, uri: str, name: str, description: str | None = None): self.resources_templates.append( diff --git a/tests/mcp/test_resources_server.py b/tests/mcp/test_resources_server.py index fe5bb8b35..bf3b81f0e 100644 --- a/tests/mcp/test_resources_server.py +++ b/tests/mcp/test_resources_server.py @@ -34,11 +34,3 @@ async def test_read_resource(): server.add_resource(AnyUrl("docs://api/reference"), name="Docs Search") await server.read_resource(AnyUrl("docs://api/reference")) - -@pytest.mark.asyncio -async def test_get_prompt_not_found(): - """Test getting a prompt that doesn't exist""" - server = FakeMCPServer() - uri = "docs://api/reference" - with pytest.raises(ValueError, match=f"Resource {uri} not found"): - await server.read_resource(AnyUrl(uri)) From a7c23b6241b830f64beeb0c9f9427f3446e4d54c Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 02:14:59 +0400 Subject: [PATCH 36/52] Adding unit tests --- tests/mcp/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mcp/helpers.py b/tests/mcp/helpers.py index 70004d7d1..227ad3731 100644 --- a/tests/mcp/helpers.py +++ b/tests/mcp/helpers.py @@ -149,4 +149,4 @@ def add_resource_template(self, uri: str, name: str, description: str | None = N @property def name(self) -> str: - return self._server_name + return self._server_name \ No newline at end of file From 0668cd98efa0804abbca7b7ee699fbb9629566dc Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 02:17:48 +0400 Subject: [PATCH 37/52] Adding unit tests --- tests/mcp/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mcp/helpers.py b/tests/mcp/helpers.py index 227ad3731..70004d7d1 100644 --- a/tests/mcp/helpers.py +++ b/tests/mcp/helpers.py @@ -149,4 +149,4 @@ def add_resource_template(self, uri: str, name: str, description: str | None = N @property def name(self) -> str: - return self._server_name \ No newline at end of file + return self._server_name From 03f465b43ff59a132de97b95f372485beab72f49 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 02:22:06 +0400 Subject: [PATCH 38/52] Adding unit tests --- tests/mcp/helpers.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/mcp/helpers.py b/tests/mcp/helpers.py index 70004d7d1..326127be7 100644 --- a/tests/mcp/helpers.py +++ b/tests/mcp/helpers.py @@ -15,6 +15,7 @@ Resource, ResourceTemplate, TextContent, + TextResourceContents, ) from pydantic import AnyUrl @@ -135,6 +136,13 @@ async def read_resource(self, uri: AnyUrl) -> ReadResourceResult: """Return a fake resource read for fake server""" for resource in self.resources: if resource.uri == uri: + dummy_contents = [ + TextResourceContents(type="text", text="dummy payload") + ] + resource = ReadResourceResult( + **resource.model_dump(exclude_none=True), + contents=dummy_contents + ) return resource raise KeyError From 767629e422a6d80afae2c6fda4c9de701e5bb84b Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 02:25:24 +0400 Subject: [PATCH 39/52] Adding unit tests --- tests/mcp/helpers.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/tests/mcp/helpers.py b/tests/mcp/helpers.py index 326127be7..f6f08f74e 100644 --- a/tests/mcp/helpers.py +++ b/tests/mcp/helpers.py @@ -136,14 +136,7 @@ async def read_resource(self, uri: AnyUrl) -> ReadResourceResult: """Return a fake resource read for fake server""" for resource in self.resources: if resource.uri == uri: - dummy_contents = [ - TextResourceContents(type="text", text="dummy payload") - ] - resource = ReadResourceResult( - **resource.model_dump(exclude_none=True), - contents=dummy_contents - ) - return resource + return ReadResourceResult(*resource.model_dump()) raise KeyError From ca1a16493d8fce30506b84705c2052cac743c5b8 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 02:25:47 +0400 Subject: [PATCH 40/52] Adding unit tests --- tests/mcp/helpers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/mcp/helpers.py b/tests/mcp/helpers.py index f6f08f74e..e32264a57 100644 --- a/tests/mcp/helpers.py +++ b/tests/mcp/helpers.py @@ -15,7 +15,6 @@ Resource, ResourceTemplate, TextContent, - TextResourceContents, ) from pydantic import AnyUrl From 9322de0a805c8f6078c3e395c3fa3d26fc283e80 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 02:27:17 +0400 Subject: [PATCH 41/52] Adding unit tests --- tests/mcp/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mcp/helpers.py b/tests/mcp/helpers.py index e32264a57..11119f6a4 100644 --- a/tests/mcp/helpers.py +++ b/tests/mcp/helpers.py @@ -135,7 +135,7 @@ async def read_resource(self, uri: AnyUrl) -> ReadResourceResult: """Return a fake resource read for fake server""" for resource in self.resources: if resource.uri == uri: - return ReadResourceResult(*resource.model_dump()) + return ReadResourceResult(*resource.model_dump(), contents=[]) raise KeyError From 38d8ac11cedf6354d7b2a80158291edd323c755f Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 02:30:20 +0400 Subject: [PATCH 42/52] Adding unit tests --- tests/mcp/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mcp/helpers.py b/tests/mcp/helpers.py index 11119f6a4..f77c9a8c6 100644 --- a/tests/mcp/helpers.py +++ b/tests/mcp/helpers.py @@ -135,7 +135,7 @@ async def read_resource(self, uri: AnyUrl) -> ReadResourceResult: """Return a fake resource read for fake server""" for resource in self.resources: if resource.uri == uri: - return ReadResourceResult(*resource.model_dump(), contents=[]) + return ReadResourceResult(**resource.model_dump(), contents=[]) raise KeyError From b8ab280ae73dd1838793cd35495279d697095db0 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 02:33:33 +0400 Subject: [PATCH 43/52] Adding unit tests --- tests/mcp/helpers.py | 8 +++++--- tests/mcp/test_resources_server.py | 9 +++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/mcp/helpers.py b/tests/mcp/helpers.py index f77c9a8c6..cb41b38e9 100644 --- a/tests/mcp/helpers.py +++ b/tests/mcp/helpers.py @@ -123,16 +123,16 @@ async def get_prompt( return GetPromptResult(description=f"Fake prompt: {name}", messages=[message]) async def list_resources(self, run_context=None, agent=None) -> ListResourcesResult: - """Return empty list of resources for fake server""" + """Return empty list of resources for the fake server""" return ListResourcesResult(resources=self.resources) async def list_resource_templates(self, run_context=None, agent=None) \ -> ListResourceTemplatesResult: - """Return empty list of resources templates for fake server""" + """Return empty list of resources templates for the fake server""" return ListResourceTemplatesResult(resourceTemplates=self.resources_templates) async def read_resource(self, uri: AnyUrl) -> ReadResourceResult: - """Return a fake resource read for fake server""" + """Return a fake resource read for the fake server""" for resource in self.resources: if resource.uri == uri: return ReadResourceResult(**resource.model_dump(), contents=[]) @@ -140,9 +140,11 @@ async def read_resource(self, uri: AnyUrl) -> ReadResourceResult: raise KeyError def add_resource(self, uri: AnyUrl, name: str, description: str | None = None): + """Add a resource to the fake server""" self.resources.append(Resource(uri=uri, description=description, name=name)) def add_resource_template(self, uri: str, name: str, description: str | None = None): + """Add a resource template to the fake server""" self.resources_templates.append( ResourceTemplate(uriTemplate=uri, description=description, name=name) ) diff --git a/tests/mcp/test_resources_server.py b/tests/mcp/test_resources_server.py index bf3b81f0e..1d27772ee 100644 --- a/tests/mcp/test_resources_server.py +++ b/tests/mcp/test_resources_server.py @@ -34,3 +34,12 @@ async def test_read_resource(): server.add_resource(AnyUrl("docs://api/reference"), name="Docs Search") await server.read_resource(AnyUrl("docs://api/reference")) + +@pytest.mark.asyncio +async def test_read_resource_not_found(): + """Test getting a resource that doesn't exist""" + server = FakeMCPServer() + + uri = AnyUrl("docs://api/reference") + with pytest.raises(ValueError, match=f"Resource {uri} not found"): + await server.read_resource(uri) From 754e364700ac299bb51b9aa9d9a78229d76dafe8 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 02:34:03 +0400 Subject: [PATCH 44/52] Adding unit tests --- tests/mcp/test_resources_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mcp/test_resources_server.py b/tests/mcp/test_resources_server.py index 1d27772ee..c3b403061 100644 --- a/tests/mcp/test_resources_server.py +++ b/tests/mcp/test_resources_server.py @@ -41,5 +41,5 @@ async def test_read_resource_not_found(): server = FakeMCPServer() uri = AnyUrl("docs://api/reference") - with pytest.raises(ValueError, match=f"Resource {uri} not found"): + with pytest.raises(KeyError, match=f"Resource {uri} not found"): await server.read_resource(uri) From a2db43a28b05ae4e3033280775d0275cd79bb380 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 02:36:15 +0400 Subject: [PATCH 45/52] Adding unit tests --- tests/mcp/helpers.py | 2 +- tests/mcp/test_resources_server.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/mcp/helpers.py b/tests/mcp/helpers.py index cb41b38e9..a5adc8b21 100644 --- a/tests/mcp/helpers.py +++ b/tests/mcp/helpers.py @@ -137,7 +137,7 @@ async def read_resource(self, uri: AnyUrl) -> ReadResourceResult: if resource.uri == uri: return ReadResourceResult(**resource.model_dump(), contents=[]) - raise KeyError + raise KeyError(f"Resource {uri} not found") def add_resource(self, uri: AnyUrl, name: str, description: str | None = None): """Add a resource to the fake server""" diff --git a/tests/mcp/test_resources_server.py b/tests/mcp/test_resources_server.py index c3b403061..5d49d6528 100644 --- a/tests/mcp/test_resources_server.py +++ b/tests/mcp/test_resources_server.py @@ -40,6 +40,6 @@ async def test_read_resource_not_found(): """Test getting a resource that doesn't exist""" server = FakeMCPServer() - uri = AnyUrl("docs://api/reference") + uri = "docs://api/reference" with pytest.raises(KeyError, match=f"Resource {uri} not found"): - await server.read_resource(uri) + await server.read_resource(AnyUrl(uri)) From e72299ae93f408c057fa7cff91f7f2b839b8ee97 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 02:42:39 +0400 Subject: [PATCH 46/52] Adding unit tests --- tests/mcp/test_resources_server.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/mcp/test_resources_server.py b/tests/mcp/test_resources_server.py index 5d49d6528..e09ca290a 100644 --- a/tests/mcp/test_resources_server.py +++ b/tests/mcp/test_resources_server.py @@ -1,6 +1,8 @@ import pytest from pydantic import AnyUrl +from agents import Agent, Runner + from .helpers import FakeMCPServer @@ -43,3 +45,27 @@ async def test_read_resource_not_found(): uri = "docs://api/reference" with pytest.raises(KeyError, match=f"Resource {uri} not found"): await server.read_resource(AnyUrl(uri)) + +@pytest.mark.asyncio +@pytest.mark.parametrize("streaming", [False, True]) +async def test_agent_with_resources(streaming: bool): + """Test agent with resources""" + server = FakeMCPServer() + + agent = Agent( + name="Assistant", + instructions="Answer users queries using the available resources", + mcp_servers=[server], + ) + + message = "What's the process to access the APIs? What are the available endpoints?" + if streaming: + streaming_result = Runner.run_streamed(agent, input=message) + async for _ in streaming_result.stream_events(): + pass + final_result = streaming_result.final_output + else: + result = await Runner.run(agent, input=message) + final_result = result.final_output + + assert final_result is not None From 8c1bbd96da99108c953580efe58f8cc6b252c1af Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 02:44:39 +0400 Subject: [PATCH 47/52] Adding unit tests --- tests/mcp/test_resources_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mcp/test_resources_server.py b/tests/mcp/test_resources_server.py index e09ca290a..6d89b4337 100644 --- a/tests/mcp/test_resources_server.py +++ b/tests/mcp/test_resources_server.py @@ -58,7 +58,7 @@ async def test_agent_with_resources(streaming: bool): mcp_servers=[server], ) - message = "What's the process to access the APIs? What are the available endpoints?" + message = "What's the process to access the APIs?" if streaming: streaming_result = Runner.run_streamed(agent, input=message) async for _ in streaming_result.stream_events(): From 84205db923dec2ad345bddaf83da5e762f41e58f Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Wed, 9 Jul 2025 02:50:30 +0400 Subject: [PATCH 48/52] Adding unit tests --- tests/mcp/test_resources_server.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/tests/mcp/test_resources_server.py b/tests/mcp/test_resources_server.py index 6d89b4337..5d49d6528 100644 --- a/tests/mcp/test_resources_server.py +++ b/tests/mcp/test_resources_server.py @@ -1,8 +1,6 @@ import pytest from pydantic import AnyUrl -from agents import Agent, Runner - from .helpers import FakeMCPServer @@ -45,27 +43,3 @@ async def test_read_resource_not_found(): uri = "docs://api/reference" with pytest.raises(KeyError, match=f"Resource {uri} not found"): await server.read_resource(AnyUrl(uri)) - -@pytest.mark.asyncio -@pytest.mark.parametrize("streaming", [False, True]) -async def test_agent_with_resources(streaming: bool): - """Test agent with resources""" - server = FakeMCPServer() - - agent = Agent( - name="Assistant", - instructions="Answer users queries using the available resources", - mcp_servers=[server], - ) - - message = "What's the process to access the APIs?" - if streaming: - streaming_result = Runner.run_streamed(agent, input=message) - async for _ in streaming_result.stream_events(): - pass - final_result = streaming_result.final_output - else: - result = await Runner.run(agent, input=message) - final_result = result.final_output - - assert final_result is not None From 02ec4a37c24d86f6a7d75e398460a01c8e20639d Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Thu, 10 Jul 2025 15:22:11 +0400 Subject: [PATCH 49/52] Remove un-necessary changes --- src/agents/mcp/server.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/agents/mcp/server.py b/src/agents/mcp/server.py index 2b94d6d48..887fc7941 100644 --- a/src/agents/mcp/server.py +++ b/src/agents/mcp/server.py @@ -297,7 +297,7 @@ async def list_tools( async def call_tool(self, tool_name: str, arguments: dict[str, Any] | None) -> CallToolResult: """Invoke a tool on the server.""" if not self.session: - raise UserError("Server not initialized. Make sure you call connect() first.") + raise UserError("Server not initialized. Make sure you call `connect()` first.") return await self.session.call_tool(tool_name, arguments) @@ -306,7 +306,7 @@ async def list_prompts( ) -> ListPromptsResult: """List the prompts available on the server.""" if not self.session: - raise UserError("Server not initialized. Make sure you call connect() first.") + raise UserError("Server not initialized. Make sure you call `connect()` first.") return await self.session.list_prompts() @@ -315,14 +315,14 @@ async def get_prompt( ) -> GetPromptResult: """Get a specific prompt from the server.""" if not self.session: - raise UserError("Server not initialized. Make sure you call connect() first.") + raise UserError("Server not initialized. Make sure you call `connect()` first.") return await self.session.get_prompt(name, arguments) async def list_resources(self, cursor: str | None = None) -> ListResourcesResult: """List the resources available on the server.""" if not self.session: - raise UserError("Server not initialized. Make sure you call connect() first.") + raise UserError("Server not initialized. Make sure you call `connect()` first.") return await self.session.list_resources(cursor) @@ -331,14 +331,14 @@ async def list_resource_templates( ) -> ListResourceTemplatesResult: """List the resources templates available on the server.""" if not self.session: - raise UserError("Server not initialized. Make sure you call connect() first.") + raise UserError("Server not initialized. Make sure you call `connect()` first.") return await self.session.list_resource_templates(cursor) async def read_resource(self, uri: AnyUrl) -> ReadResourceResult: """Read a specific resource given its uri.""" if not self.session: - raise UserError("Server not initialized. Make sure you call connect() first.") + raise UserError("Server not initialized. Make sure you call `connect()` first.") return await self.session.read_resource(uri) From 93a90454e6e14feca7c806d75f0cd2cb7cfb95dc Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Thu, 10 Jul 2025 15:22:56 +0400 Subject: [PATCH 50/52] Remove un-necessary changes --- 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 887fc7941..c6a8f28eb 100644 --- a/src/agents/mcp/server.py +++ b/src/agents/mcp/server.py @@ -274,7 +274,7 @@ async def list_tools( ) -> list[MCPTool]: """List the tools available on the server.""" if not self.session: - raise UserError("Server not initialized. Make sure you call connect() first.") + 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: From c9233bc94d637e9e96113fa038c89963e5ed5a46 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Sat, 12 Jul 2025 10:14:48 +0400 Subject: [PATCH 51/52] Increase coverage --- tests/test_handoff_tool.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/test_handoff_tool.py b/tests/test_handoff_tool.py index 0f7fc2166..1808a5ba4 100644 --- a/tests/test_handoff_tool.py +++ b/tests/test_handoff_tool.py @@ -12,9 +12,11 @@ MessageOutputItem, ModelBehaviorError, RunContextWrapper, + Runner, UserError, handoff, ) +from agents.extensions.handoff_prompt import prompt_with_handoff_instructions, RECOMMENDED_PROMPT_PREFIX from agents.run import AgentRunner @@ -370,3 +372,32 @@ async def test_handoff_is_enabled_filtering_integration(): agent_names = {h.agent_name for h in filtered_handoffs} assert agent_names == {"agent_1", "agent_3"} assert "agent_2" not in agent_names + +@pytest.mark.asyncio +async def test_handoff_prompt_is_passed_to_model(): + agent_1 = Agent(name="agent_1", instructions="Respond by only hello") + agent_2 = Agent(name="agent_2") + agent_3 = Agent(name="agent_3") + + instructions = prompt_with_handoff_instructions( + "Greetings should be addressed by agent_1") + + main_agent = Agent( + name="main_agent", + instructions=instructions, + handoffs=[ + handoff(agent_1), + handoff(agent_2), + handoff(agent_3), + ], + ) + + result = await Runner.run(main_agent, input="Hi, my name is Sora.") + assert result.final_output == "hello" + +@pytest.mark.asyncio +async def test_handoff_prompt_is_correct(): + n = len(RECOMMENDED_PROMPT_PREFIX) + assert (RECOMMENDED_PROMPT_PREFIX == + prompt_with_handoff_instructions("Respond to user queries") + [:n]) From b9213c85482697581738e6161cb1e8c8c273f967 Mon Sep 17 00:00:00 2001 From: Mohamed Amri Date: Sat, 12 Jul 2025 10:27:59 +0400 Subject: [PATCH 52/52] Add coverage --- tests/test_handoff_tool.py | 28 ++++------------------------ 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/tests/test_handoff_tool.py b/tests/test_handoff_tool.py index 1808a5ba4..0df0f0eb0 100644 --- a/tests/test_handoff_tool.py +++ b/tests/test_handoff_tool.py @@ -12,11 +12,13 @@ MessageOutputItem, ModelBehaviorError, RunContextWrapper, - Runner, UserError, handoff, ) -from agents.extensions.handoff_prompt import prompt_with_handoff_instructions, RECOMMENDED_PROMPT_PREFIX +from agents.extensions.handoff_prompt import ( + RECOMMENDED_PROMPT_PREFIX, + prompt_with_handoff_instructions, +) from agents.run import AgentRunner @@ -373,28 +375,6 @@ async def test_handoff_is_enabled_filtering_integration(): assert agent_names == {"agent_1", "agent_3"} assert "agent_2" not in agent_names -@pytest.mark.asyncio -async def test_handoff_prompt_is_passed_to_model(): - agent_1 = Agent(name="agent_1", instructions="Respond by only hello") - agent_2 = Agent(name="agent_2") - agent_3 = Agent(name="agent_3") - - instructions = prompt_with_handoff_instructions( - "Greetings should be addressed by agent_1") - - main_agent = Agent( - name="main_agent", - instructions=instructions, - handoffs=[ - handoff(agent_1), - handoff(agent_2), - handoff(agent_3), - ], - ) - - result = await Runner.run(main_agent, input="Hi, my name is Sora.") - assert result.final_output == "hello" - @pytest.mark.asyncio async def test_handoff_prompt_is_correct(): n = len(RECOMMENDED_PROMPT_PREFIX)