Skip to content

Feat/support mcp resources #1042

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 55 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
b7fbb77
Adding an example
amri369 Jul 8, 2025
2ae7c3b
Finish resources examples
amri369 Jul 8, 2025
c2e9e4c
Add documentation
amri369 Jul 8, 2025
1b1a55f
Update global docs
amri369 Jul 8, 2025
a812d24
Linting
amri369 Jul 8, 2025
0be886e
Linting
amri369 Jul 8, 2025
369da53
Fix lint
amri369 Jul 8, 2025
adedd7e
Fix lint
amri369 Jul 8, 2025
b6ce45b
Fix types
amri369 Jul 8, 2025
6a919e1
Fix types
amri369 Jul 8, 2025
db08e8c
Fix typing
amri369 Jul 8, 2025
a4ddd01
Fix typing
amri369 Jul 8, 2025
cfd050f
Fix typing
amri369 Jul 8, 2025
7ab231a
Fix typing
amri369 Jul 8, 2025
7e19afa
Fix unit tests
amri369 Jul 8, 2025
7d4a58b
Adding unit tests
amri369 Jul 8, 2025
c654f20
Adding unit tests
amri369 Jul 8, 2025
107b584
Adding unit tests
amri369 Jul 8, 2025
27a0465
Adding unit tests
amri369 Jul 8, 2025
b8eed9d
Adding unit tests
amri369 Jul 8, 2025
8a581c6
Adding unit tests
amri369 Jul 8, 2025
28255ac
Adding unit tests
amri369 Jul 8, 2025
68a1924
Adding unit tests
amri369 Jul 8, 2025
129dad5
Adding unit tests
amri369 Jul 8, 2025
649f861
Improve example
amri369 Jul 8, 2025
5dc6caf
Adding unit tests
amri369 Jul 8, 2025
4e3b0fc
Adding unit tests
amri369 Jul 8, 2025
4e37bde
Adding unit tests
amri369 Jul 8, 2025
b4cf67c
Adding unit tests
amri369 Jul 8, 2025
8382b84
Adding unit tests
amri369 Jul 8, 2025
2c09122
Adding unit tests
amri369 Jul 8, 2025
aaef281
Adding unit tests
amri369 Jul 8, 2025
c23735c
Adding unit tests
amri369 Jul 8, 2025
b2fa63c
Adding unit tests
amri369 Jul 8, 2025
e0f8ee2
Adding unit tests
amri369 Jul 8, 2025
a7c23b6
Adding unit tests
amri369 Jul 8, 2025
0668cd9
Adding unit tests
amri369 Jul 8, 2025
03f465b
Adding unit tests
amri369 Jul 8, 2025
767629e
Adding unit tests
amri369 Jul 8, 2025
ca1a164
Adding unit tests
amri369 Jul 8, 2025
9322de0
Adding unit tests
amri369 Jul 8, 2025
38d8ac1
Adding unit tests
amri369 Jul 8, 2025
b8ab280
Adding unit tests
amri369 Jul 8, 2025
754e364
Adding unit tests
amri369 Jul 8, 2025
a2db43a
Adding unit tests
amri369 Jul 8, 2025
e72299a
Adding unit tests
amri369 Jul 8, 2025
8c1bbd9
Adding unit tests
amri369 Jul 8, 2025
84205db
Adding unit tests
amri369 Jul 8, 2025
02ec4a3
Remove un-necessary changes
amri369 Jul 10, 2025
93a9045
Remove un-necessary changes
amri369 Jul 10, 2025
496803e
Merge branch 'main' into feat/support-mcp-resources
amri369 Jul 11, 2025
c9233bc
Increase coverage
amri369 Jul 12, 2025
b9213c8
Add coverage
amri369 Jul 12, 2025
cfd8934
Merge branch 'main' into feat/support-mcp-resources
amri369 Jul 12, 2025
146cb12
Merge branch 'main' into feat/support-mcp-resources
amri369 Jul 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions docs/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
19 changes: 19 additions & 0 deletions examples/mcp/resources_server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# 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.

The local MCP Resources Server is defined in [server.py](server.py).

Run the example via:

```
uv run python examples/mcp/resources_server/main.py
```

## 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 resources that can be exposed to agents.
The example demonstrates three main functions:

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.
81 changes: 81 additions & 0 deletions examples/mcp/resources_server/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import asyncio
import os
import shutil
import subprocess
import time
from typing import Any

from mcp.types import ListResourcesResult, ListResourceTemplatesResult, ReadResourceResult
from pydantic import AnyUrl

from agents import Agent, Runner, gen_trace_id, trace
from agents.mcp import MCPServer, MCPServerStreamableHttp


async def list_resources(mcp_server: MCPServer):
"""List available 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):
"""List available resources 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}")

async def read_resource(mcp_server: MCPServer, uri: AnyUrl):
resource: ReadResourceResult = await mcp_server.read_resource(uri)
print("\n### Resource Content ###")
print(resource.contents[0])

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_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")

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.")
101 changes: 101 additions & 0 deletions examples/mcp/resources_server/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("Resources Server")

API_REFERENCE_MD = """
# Company API Reference

## Authentication
Use the `Authorization: Bearer <token>` 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")
51 changes: 50 additions & 1 deletion src/agents/mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,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,
GetPromptResult,
InitializeResult,
ListPromptsResult,
ListResourcesResult,
ListResourceTemplatesResult,
ReadResourceResult,
)
from pydantic import AnyUrl
from typing_extensions import NotRequired, TypedDict

from ..exceptions import UserError
Expand Down Expand Up @@ -77,6 +86,23 @@ 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


class _MCPServerWithClientSession(MCPServer, abc.ABC):
"""Base class for MCP servers that use a `ClientSession` to communicate with the server."""
Expand Down Expand Up @@ -293,6 +319,29 @@ async def get_prompt(

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.")

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."""
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."""
if not self.session:
raise UserError("Server not initialized. Make sure you call `connect()` first.")

return await self.session.read_resource(uri)

async def cleanup(self):
"""Cleanup the server."""
async with self._cleanup_lock:
Expand Down
45 changes: 44 additions & 1 deletion tests/mcp/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,19 @@
from typing import Any

from mcp import Tool as MCPTool
from mcp.types import CallToolResult, GetPromptResult, ListPromptsResult, PromptMessage, TextContent
from mcp.types import (
CallToolResult,
GetPromptResult,
ListPromptsResult,
ListResourcesResult,
ListResourceTemplatesResult,
PromptMessage,
ReadResourceResult,
Resource,
ResourceTemplate,
TextContent,
)
from pydantic import AnyUrl

from agents.mcp import MCPServer
from agents.mcp.server import _MCPServerWithClientSession
Expand Down Expand Up @@ -57,10 +69,14 @@ def name(self) -> str:
class FakeMCPServer(MCPServer):
def __init__(
self,
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 []
self.resources_templates = resources_templates or []
self.tools: list[MCPTool] = tools or []
self.tool_calls: list[str] = []
self.tool_results: list[str] = []
Expand Down Expand Up @@ -106,6 +122,33 @@ 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 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 the fake server"""
return ListResourceTemplatesResult(resourceTemplates=self.resources_templates)

async def read_resource(self, uri: AnyUrl) -> ReadResourceResult:
"""Return a fake resource read for the fake server"""
for resource in self.resources:
if resource.uri == uri:
return ReadResourceResult(**resource.model_dump(), contents=[])

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"""
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)
)

@property
def name(self) -> str:
return self._server_name
Loading